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}