Skip to main content

media_pp/elements/driver/webrtc/
track.rs

1use std::{
2    sync::{
3        Arc, Mutex,
4        atomic::{AtomicU64, Ordering},
5    },
6    time::Duration,
7};
8
9use crate::pp_log::{PpLog, pp_error, pp_info};
10use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TrySendError, select};
11use str0m::{
12    change::{SdpAnswer, SdpOffer},
13    format::Codec,
14    media::{Direction, MediaKind, Mid},
15};
16
17use crate::{
18    buffer::MediaBuffer,
19    bus::{Bus, BusEvent},
20    control::{
21        ControlMsg, ControlReceiver, RequestKind, apply_finish, apply_one, drain_control,
22        wait_out_pause,
23    },
24    element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
25    error::Result,
26    pad::SrcPad,
27};
28
29use super::{
30    command::{Command, TrackId, WebRtcError},
31    stream_info::WebRtcStreamInfo,
32};
33
34/// The endpoints a track actually has, which is exactly what its
35/// negotiated [`Direction`] allows — a `SendOnly` track carries no
36/// `WebRtcTrackSource` because nothing will ever arrive on it, and a
37/// `RecvOnly` one carries no [`WebRtcTrackSink`] because str0m has no
38/// send capability for it.
39///
40/// The variant *is* the direction, so there is no separate field the two
41/// could disagree with. Pushing into a sink that does not exist, or
42/// waiting on a source that does not, is a compile error rather than
43/// something that silently does nothing.
44///
45/// Fixed for the life of the track: these are handed out once, when the
46/// track attaches, and a remote peer that later renegotiates a different
47/// direction is reported on the [`Bus`] instead (see
48/// [`WebRtcHandle::next_track`]).
49pub enum TrackEndpoints {
50    /// `Direction::SendOnly` — outbound only.
51    Send(WebRtcTrackSink),
52    /// `Direction::RecvOnly` — inbound only.
53    Recv(WebRtcTrackSource),
54    /// `Direction::SendRecv` — both, on the one track.
55    SendRecv(WebRtcTrackSink, WebRtcTrackSource),
56    /// `Direction::Inactive` — neither, for now. Still handed out: the
57    /// track exists and its `mid` is negotiated, so a caller matching
58    /// attachments against its own [`WebRtcHandle::add_track`] calls has
59    /// to see it.
60    Inactive,
61}
62
63/// One newly-attached track, from [`WebRtcHandle::next_track`].
64pub struct AttachedTrack {
65    /// Matches what [`WebRtcHandle::add_track`] returned for a track this
66    /// side requested. A track the *remote* peer added has an id issued
67    /// here that the caller has never seen before — which is how the two
68    /// are told apart.
69    pub id: TrackId,
70    /// The `mid` str0m assigned during the SDP exchange.
71    pub mid: Mid,
72    /// Audio or video.
73    pub kind: MediaKind,
74    /// What can actually be done with this track — see [`TrackEndpoints`].
75    pub endpoints: TrackEndpoints,
76}
77
78/// Cheaply-cloneable handle for requesting new tracks, completing
79/// renegotiation, and picking up newly-attached tracks — same spirit as
80/// [`crate::elements::AppSourceHandle`]. Cloning shares one queue of
81/// pending [`WebRtcHandle::next_track`] results, same as any other
82/// multi-consumer channel — only one clone's call actually receives a
83/// given track, so in practice only one place in the app should be
84/// draining it.
85#[derive(Clone)]
86pub struct WebRtcHandle {
87    pub(super) next_id: Arc<AtomicU64>,
88    pub(super) command_tx: Sender<Command>,
89    pub(super) new_track_rx: Receiver<AttachedTrack>,
90}
91
92impl WebRtcHandle {
93    /// Requests a new track of `kind`/`direction`. Blocks only while the
94    /// peer's bounded command queue is full; once the command is accepted,
95    /// returns the locally assigned [`TrackId`]. This does not mean SDP
96    /// negotiation has completed — receive the attached track through
97    /// [`WebRtcHandle::next_track`]. Returns [`WebRtcError::Closed`] without
98    /// yielding a `TrackId` if the peer loop has already stopped.
99    ///
100    /// `codec` is what [`WebRtcTrackSink::consume`] on the resulting track
101    /// will actually be fed (an encoder's output, or a packet relayed
102    /// verbatim from another track) — used to pick the matching payload
103    /// type out of whatever this connection negotiates for the track,
104    /// instead of guessing. If this connection does not negotiate `codec`,
105    /// consuming a packet returns
106    /// [`WebRtcError::OutboundCodecNotNegotiated`].
107    ///
108    /// Declaring one codec and pushing another is not detected anywhere:
109    /// str0m packetizes whatever bytes it is handed under the payload type
110    /// chosen here, so the mismatch leaves as a well-formed stream that no
111    /// receiver can decode. Audio is where this bites — WebRTC negotiates
112    /// Opus, and there is no AAC payload type to fall back to.
113    pub fn add_track(
114        &self,
115        kind: MediaKind,
116        direction: Direction,
117        codec: Codec,
118    ) -> Result<TrackId> {
119        let id = TrackId(self.next_id.fetch_add(1, Ordering::Relaxed));
120        self.command_tx
121            .send(Command::AddTrack(id, kind, direction, codec))
122            .map_err(|_| WebRtcError::Closed)?;
123        Ok(id)
124    }
125
126    /// Blocks until the next track attaches — either one requested via
127    /// [`WebRtcHandle::add_track`] (on either side) once its `Mid` exists,
128    /// or one the remote peer added on its own. `Err` once `WebRtcPeer`
129    /// (and its `run`) is gone and every already-attached track has been
130    /// drained.
131    ///
132    /// Which track this is has to be established from
133    /// [`AttachedTrack::id`]: a remote peer adding a track of its own is
134    /// delivered through this same queue, so "the call right after my
135    /// `add_track`" is not a guarantee of anything. Match the id against
136    /// what `add_track` returned.
137    ///
138    /// [`AttachedTrack::endpoints`] carries only what the track's
139    /// negotiated direction actually permits. That direction is read once,
140    /// as the track attaches, and the endpoints are never re-issued — so a
141    /// remote peer that renegotiates a different direction afterwards
142    /// makes them wrong. That case is reported as
143    /// [`WebRtcError::DirectionChanged`] on the [`Bus`] rather than
144    /// silently tolerated; recovering from it means tearing the track down
145    /// and adding a new one.
146    ///
147    /// Both endpoints expose their currently negotiated codec lists. A send
148    /// endpoint for a track this side requested already selects the codec
149    /// passed to [`WebRtcHandle::add_track`]. For a track the remote side
150    /// added, choose the application's encoder output from
151    /// [`WebRtcTrackSink::negotiated_codecs`] and pass it to
152    /// [`WebRtcTrackSink::set_codec`] before pushing packets. The matching
153    /// source separately reports the codec actually received once RTP starts.
154    pub fn next_track(&self) -> Result<AttachedTrack> {
155        self.new_track_rx
156            .recv()
157            .map_err(|_| WebRtcError::Closed.into())
158    }
159
160    /// Feeds a remote answer back in, completing a renegotiation started by
161    /// [`WebRtcHandle::add_track`]. A no-op if `WebRtcPeer` (and its `run`)
162    /// is already gone.
163    pub fn set_answer(&self, answer: SdpAnswer) {
164        let _ = self.command_tx.send(Command::SetAnswer(answer));
165    }
166
167    /// Accepts a fresh offer from the *remote* peer (their own
168    /// renegotiation) and returns the resulting answer for the caller to
169    /// ship back over its own signaling transport. Blocks until
170    /// `WebRtcPeer::run` has actually applied it.
171    pub fn accept_remote_offer(&self, offer: SdpOffer) -> Result<SdpAnswer> {
172        let (reply_tx, reply_rx) = crossbeam_channel::bounded(0);
173        self.command_tx
174            .send(Command::AcceptOffer(offer, reply_tx))
175            .map_err(|_| WebRtcError::Closed)?;
176        reply_rx
177            .recv()
178            .map_err(|_| WebRtcError::Closed)?
179            .map_err(Into::into)
180    }
181}
182
183/// One outbound track. A plain [`Sink`] — no bespoke push API, it links
184/// into a [`crate::pipeline::ChainBuilder`] exactly like
185/// [`crate::elements::RtspSink`] or any other terminal sink.
186/// `consume()` only ever hands off to `WebRtcPeer::run`'s own thread via a
187/// channel send; the actual str0m write happens over there.
188///
189/// Its negotiated codec capabilities are available immediately through
190/// [`WebRtcTrackSink::negotiated_codecs`]. The outbound selection is initialized
191/// automatically for a track created by [`WebRtcHandle::add_track`]. A
192/// send-capable track added by the remote peer instead requires one validated
193/// [`WebRtcTrackSink::set_codec`] call before packets are consumed; omitting it
194/// returns a typed error rather than guessing an RTP payload type.
195pub struct WebRtcTrackSink {
196    pp_log: PpLog,
197    id: TrackId,
198    codec: Option<Codec>,
199    negotiated_codecs: Arc<Mutex<Vec<Codec>>>,
200    command_tx: Sender<Command>,
201    /// libavcodec may express encoder delay as a negative first PTS (Opus is
202    /// a common example), while RTP media time is unsigned. The first packet
203    /// establishes one track-wide shift so relative timing is preserved.
204    timestamp_offset: Option<i64>,
205}
206
207impl WebRtcTrackSink {
208    pub(super) fn new(
209        id: TrackId,
210        codec: Option<Codec>,
211        negotiated_codecs: Arc<Mutex<Vec<Codec>>>,
212        command_tx: Sender<Command>,
213    ) -> Self {
214        Self {
215            id,
216            codec,
217            negotiated_codecs,
218            command_tx,
219            timestamp_offset: None,
220            pp_log: element_pp_log(
221                ElementType::WebRtcPeer,
222                &format!("webrtc-track-{}", id.0),
223                None,
224            ),
225        }
226    }
227
228    /// Returns the distinct codec families this track can currently send
229    /// after SDP negotiation. The order is informational; select the codec
230    /// produced by the application's encoder.
231    ///
232    /// A locally-created endpoint is handed out while its offer is still
233    /// pending, so its initial value is the offered list and is narrowed when
234    /// [`WebRtcHandle::set_answer`] applies the answer. A remotely-created
235    /// endpoint is already negotiated when it is handed out.
236    pub fn negotiated_codecs(&self) -> Vec<Codec> {
237        self.negotiated_codecs.lock().unwrap().clone()
238    }
239
240    /// Declares the codec carried by packets pushed into this sink.
241    ///
242    /// A sink returned for this side's own [`WebRtcHandle::add_track`] call
243    /// is initialized from that call's `codec`. A send-capable track added
244    /// by the remote peer cannot be initialized automatically: one SDP media
245    /// section can negotiate several codecs, and only this application knows
246    /// which encoder feeds its outbound half. Call this before pushing a
247    /// packet into such a sink; the choice is validated against
248    /// [`WebRtcTrackSink::negotiated_codecs`]. If no choice is made,
249    /// [`Sink::consume`] returns [`WebRtcError::OutboundCodecNotDeclared`]
250    /// instead of guessing a payload type and emitting a mislabeled RTP
251    /// stream.
252    ///
253    /// Returns [`WebRtcError::OutboundCodecNotNegotiated`] without changing
254    /// the previous selection when `codec` is unavailable. Already-enqueued
255    /// packets retain the declaration they were submitted with.
256    pub fn set_codec(&mut self, codec: Codec) -> Result<()> {
257        let negotiated = self.negotiated_codecs();
258        if !negotiated.contains(&codec) {
259            return Err(WebRtcError::OutboundCodecNotNegotiated {
260                track_id: self.id,
261                codec,
262                negotiated,
263            }
264            .into());
265        }
266        self.codec = Some(codec);
267        Ok(())
268    }
269}
270
271impl Element for WebRtcTrackSink {
272    fn name(&self) -> Arc<str> {
273        format!("webrtc-track-{}", self.id.0).into()
274    }
275
276    fn element_type(&self) -> ElementType {
277        ElementType::WebRtcPeer
278    }
279
280    fn pp_log(&self) -> &PpLog {
281        &self.pp_log
282    }
283
284    fn pp_log_mut(&mut self) -> &mut PpLog {
285        &mut self.pp_log
286    }
287}
288
289impl Sink for WebRtcTrackSink {
290    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
291        if !matches!(buf, MediaBuffer::Packet(_) | MediaBuffer::Eos) {
292            let kind = match buf {
293                MediaBuffer::Video(_) => "Video",
294                MediaBuffer::Audio(_) => "Audio",
295                MediaBuffer::Packet(_) | MediaBuffer::Eos => unreachable!("matched above"),
296            };
297            pp_error!(self, "unsupported buffer: {kind}");
298            return Err(WebRtcError::UnsupportedBuffer(kind).into());
299        }
300        if matches!(buf, MediaBuffer::Packet(_)) && self.codec.is_none() {
301            pp_error!(self, "outbound codec is not declared");
302            return Err(WebRtcError::OutboundCodecNotDeclared(self.id).into());
303        }
304        if let MediaBuffer::Packet(_) = &buf {
305            let codec = self.codec.expect("checked above");
306            let negotiated = self.negotiated_codecs();
307            if !negotiated.contains(&codec) {
308                pp_error!(self, "outbound codec {codec:?} is not negotiated");
309                return Err(WebRtcError::OutboundCodecNotNegotiated {
310                    track_id: self.id,
311                    codec,
312                    negotiated,
313                }
314                .into());
315            }
316        }
317        let buf = self.normalize_packet_timestamp(buf)?;
318        // `WebRtcPeer::run` gone (channel disconnected) means this track is
319        // dead — surface it as `Err` rather than swallowing it, so whatever
320        // pipeline this `Sink` is plugged into (its own `Queue`, its own
321        // `Bus`) actually learns about it instead of silently sending into
322        // a void forever. Non-fatal by the same convention as any other
323        // `Sink::consume` failure (see `Queue`'s own docs) — just no longer
324        // an invisible one.
325        //
326        // A full channel (`WebRtcPeer::run` backed up) drops the newest
327        // buffer instead — same as an unopened track (see `add_track`'s
328        // docs) — but isn't reported on a `Bus`: unlike `WebRtcPeer::run`,
329        // which only ever borrows a `Bus` for the duration of one `run()`
330        // call, `WebRtcTrackSink` is a handle the caller can keep past
331        // `Driver::stop()`, so storing one here would keep that `Bus`'s
332        // channel open indefinitely — including past whatever's waiting on
333        // `BusReceiver::iter()` to finish once every sender is gone.
334        match self
335            .command_tx
336            .try_send(Command::Push(self.id, self.codec, buf))
337        {
338            Ok(()) | Err(TrySendError::Full(_)) => Ok(()),
339            Err(TrySendError::Disconnected(_)) => {
340                pp_error!(self, "WebRtcPeer::run gone — track is dead");
341                Err(WebRtcError::Closed.into())
342            }
343        }
344    }
345
346    fn control(&mut self, _msg: ControlMsg) -> Result<()> {
347        // Terminal, same as AppSink/RtspSink: nothing buffered or
348        // downstream to flush/forward for any ControlMsg.
349        Ok(())
350    }
351}
352
353impl WebRtcTrackSink {
354    fn normalize_packet_timestamp(&mut self, buf: MediaBuffer) -> Result<MediaBuffer> {
355        let MediaBuffer::Packet(packet) = buf else {
356            return Ok(buf);
357        };
358        let Some(pts) = packet.pts() else {
359            return Ok(MediaBuffer::Packet(packet));
360        };
361        let offset = match self.timestamp_offset {
362            Some(offset) => offset,
363            None if pts < 0 => {
364                pts.checked_neg()
365                    .ok_or(WebRtcError::PacketTimestampNormalizationOverflow {
366                        value: pts,
367                        offset: 0,
368                    })?
369            }
370            None => 0,
371        };
372        self.timestamp_offset = Some(offset);
373        if offset == 0 {
374            return Ok(MediaBuffer::Packet(packet));
375        }
376
377        let shifted = |value: i64| {
378            value
379                .checked_add(offset)
380                .ok_or(WebRtcError::PacketTimestampNormalizationOverflow { value, offset })
381        };
382        let mut normalized = (*packet).clone();
383        normalized.set_pts(Some(shifted(pts)?));
384        normalized.set_dts(packet.dts().map(shifted).transpose()?);
385        Ok(MediaBuffer::Packet(Arc::new(normalized)))
386    }
387}
388
389/// One inbound track — the mirror image of [`WebRtcTrackSink`]. A plain
390/// [`SourceElement`], same shape as [`crate::elements::AppSource`]: it
391/// links into its own [`crate::pipeline::Pipeline`] via `src_pads()` like
392/// any other source. The difference from `AppSource` is only *who* feeds
393/// it — instead of an [`crate::elements::AppSourceHandle`] the app calls
394/// itself, [`crate::driver::Driver::run`] pushes into the sending half of this same
395/// channel internally, from its own thread, for every `Event::MediaData`
396/// on this track's `Mid`. Nothing here ever calls back into caller-supplied
397/// code from `WebRtcPeer::run`'s own thread — that thread only ever touches
398/// this crate's own types (see the module docs for why `WebRtcPeer` hands
399/// tracks out through [`WebRtcHandle::next_track`] instead of a callback).
400pub struct WebRtcTrackSource {
401    id: TrackId,
402    pp_log: PpLog,
403    name: Arc<str>,
404    pad: SrcPad,
405    data_rx: Receiver<MediaBuffer>,
406    codec: Arc<Mutex<Option<Codec>>>,
407    negotiated_codecs: Arc<Mutex<Vec<Codec>>>,
408    stream_info: Mutex<StreamInfoState>,
409}
410
411struct StreamInfoState {
412    rx: Receiver<WebRtcStreamInfo>,
413    cached: Option<WebRtcStreamInfo>,
414}
415
416impl WebRtcTrackSource {
417    pub(super) fn new(
418        id: TrackId,
419        name: impl Into<String>,
420        data_rx: Receiver<MediaBuffer>,
421        codec: Arc<Mutex<Option<Codec>>>,
422        negotiated_codecs: Arc<Mutex<Vec<Codec>>>,
423        stream_info_rx: Receiver<WebRtcStreamInfo>,
424    ) -> Self {
425        let name: Arc<str> = name.into().into();
426        let pp_log = element_pp_log(ElementType::WebRtcPeer, &name, None);
427        let pad = SrcPad::new(format!("{name}_src"));
428        Self {
429            id,
430            name,
431            pp_log,
432            pad,
433            data_rx,
434            codec,
435            negotiated_codecs,
436            stream_info: Mutex::new(StreamInfoState {
437                rx: stream_info_rx,
438                cached: None,
439            }),
440        }
441    }
442
443    /// Blocks for at most `timeout` until actual RTP media confirms enough
444    /// stream parameters to construct downstream consumers. Most codecs are
445    /// known from the first payload; H.264 waits until both SPS and PPS have
446    /// arrived. The returned [`WebRtcStreamInfo`] can derive the RTP time base
447    /// and FFmpeg parameters for a decoder or supported muxer.
448    ///
449    /// A timeout returns [`WebRtcError::StreamInfoTimeout`] without consuming
450    /// or invalidating anything, so the caller may retry. Once confirmed, the
451    /// value is cached and every later call returns it immediately. If the
452    /// peer closes before the required media information arrives, this returns
453    /// [`WebRtcError::Closed`]. This method does not consume media packets:
454    /// they remain buffered for [`SourceElement::run`].
455    pub fn wait_stream_info(&self, timeout: Duration) -> Result<WebRtcStreamInfo> {
456        let mut state = self.stream_info.lock().unwrap();
457        if let Some(info) = &state.cached {
458            return Ok(info.clone());
459        }
460
461        match state.rx.recv_timeout(timeout) {
462            Ok(info) => {
463                state.cached = Some(info.clone());
464                Ok(info)
465            }
466            Err(RecvTimeoutError::Timeout) => Err(WebRtcError::StreamInfoTimeout {
467                track_id: self.id,
468                timeout,
469            }
470            .into()),
471            Err(RecvTimeoutError::Disconnected) => Err(WebRtcError::Closed.into()),
472        }
473    }
474
475    /// Returns the distinct codec families this track can currently receive
476    /// after SDP negotiation. The order is informational.
477    ///
478    /// This is available as soon as the source is created. For a source on
479    /// the side that originated the media section, the initial offered list
480    /// is narrowed when [`WebRtcHandle::set_answer`] applies the answer.
481    /// [`WebRtcTrackSource::codec`] remains separate: it reports which codec
482    /// the remote sender actually chose once media starts arriving.
483    pub fn negotiated_codecs(&self) -> Vec<Codec> {
484        self.negotiated_codecs.lock().unwrap().clone()
485    }
486
487    /// The codec this track is actually carrying, as seen on the most
488    /// recently received packet's RTP payload type — `None` until the
489    /// first one arrives. Unlike [`WebRtcHandle::add_track`]'s `codec`
490    /// (which the *caller* declares up front for an outbound track), an
491    /// inbound track's codec isn't knowable ahead of time: SDP negotiation
492    /// can accept several codecs for one `m=` line, and only the packets
493    /// actually arriving say which one the remote side picked (see
494    /// `Event::MediaData`'s own `params` field). Whatever's downstream
495    /// (e.g. a decoder) needs a keyframe before it can do anything useful
496    /// anyway, so waiting for the first packet to learn the codec isn't an
497    /// extra constraint in practice. Use [`Self::wait_stream_info`] when the
498    /// downstream graph must be configured before this source starts running.
499    pub fn codec(&self) -> Option<Codec> {
500        *self.codec.lock().unwrap()
501    }
502}
503
504impl Element for WebRtcTrackSource {
505    fn name(&self) -> Arc<str> {
506        self.name.clone()
507    }
508
509    fn element_type(&self) -> ElementType {
510        ElementType::WebRtcPeer
511    }
512
513    fn pp_log(&self) -> &PpLog {
514        &self.pp_log
515    }
516
517    fn pp_log_mut(&mut self) -> &mut PpLog {
518        &mut self.pp_log
519    }
520}
521
522impl Source for WebRtcTrackSource {
523    fn src_pads(&mut self) -> &mut [SrcPad] {
524        std::slice::from_mut(&mut self.pad)
525    }
526}
527
528impl SourceElement for WebRtcTrackSource {
529    /// Identical shape to [`crate::elements::AppSource::run`]: selects on
530    /// `control` and its own data channel together, so `Stop`/`Pause`
531    /// never wait behind a remote peer that's gone quiet. The data channel
532    /// disconnecting — `WebRtcPeer` gone, whether from `Stop` or the
533    /// connection dying on its own — ends this the same way `AppSource`
534    /// ends when every `AppSourceHandle` is dropped: one final `Eos`, no
535    /// error.
536    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
537        pp_info!(self, "started");
538        loop {
539            if drain_control(control, self, bus)?.stopped {
540                pp_info!(self, "stopped");
541                return Ok(());
542            }
543
544            select! {
545                recv(control.rx) -> req => {
546                    match req {
547                        Ok(req) => {
548                            match req.kind {
549                                RequestKind::Finish => {
550                                    apply_finish(self, bus, &req.ack);
551                                    pp_info!(self, "finished");
552                                    return Ok(());
553                                }
554                                RequestKind::Control(msg) => {
555                                    if apply_one(self, bus, msg, &req.ack)? {
556                                        pp_info!(self, "stopped");
557                                        return Ok(());
558                                    }
559                                    if msg == ControlMsg::Pause
560                                        && wait_out_pause(control, self, bus)?
561                                    {
562                                        pp_info!(self, "stopped");
563                                        return Ok(());
564                                    }
565                                }
566                            }
567                        }
568                        // The Pipeline itself is gone — nothing left to drive this.
569                        Err(_) => {
570                            pp_info!(self, "run: control channel gone, ending");
571                            return Ok(());
572                        }
573                    }
574                }
575                recv(self.data_rx) -> buf => {
576                    match buf {
577                        Ok(buf) if buf.is_eos() => {
578                            pp_info!(self, "event=eos phase=source_received");
579                            break;
580                        }
581                        Ok(buf) => {
582                            if let Err(error) = self.pad.push(buf) {
583                                bus.post(
584                                    &self.pp_log,
585                                    BusEvent::Error {
586                                        element_type: ElementType::WebRtcPeer,
587                                        name: self.name.clone(),
588                                        error,
589                                    },
590                                );
591                            }
592                        }
593                        // `WebRtcPeer` gone — this track (or the whole peer) is done.
594                        Err(_) => {
595                            pp_info!(self, "run: WebRtcPeer gone, ending");
596                            break;
597                        }
598                    }
599                }
600            }
601        }
602        // The data channel ending (above) can race a `Stop` sent at the
603        // same moment — e.g. stopping the *upstream* `WebRtcPeer` (via its
604        // `DriverRunner`) disconnects this exact channel, and a caller
605        // stopping this `Pipeline` too, right after, can land its `Stop` in
606        // `control`'s queue after `select!` already picked the data arm.
607        // Ack it (a no-op otherwise) so `ControlSender::send`'s rendezvous
608        // never blocks forever waiting for an ack this thread would
609        // otherwise never get around to sending.
610        while let Some((_msg, ack)) = control.try_recv() {
611            let _ = ack.send(());
612        }
613        self.pad.push_eos(&self.pp_log)
614    }
615
616    /// No timeline of its own — same reasoning as
617    /// [`crate::elements::AppSource::seek`]: a WebRTC connection has
618    /// nothing to reposition.
619    fn seek(&mut self, target: Duration) -> Result<Duration> {
620        Ok(target)
621    }
622}