Skip to main content

media_pp\core\pipeline/
runtime.rs

1use std::{
2    sync::{
3        Arc, Mutex,
4        atomic::{AtomicBool, AtomicUsize, Ordering},
5    },
6    thread::{self, JoinHandle},
7    time::Duration,
8};
9
10use crate::pp_log::{PpLog, pp_info, pp_trace};
11
12use crate::{
13    bus::{Bus, BusEvent, BusReceiver},
14    clock::Clock,
15    control::{ControlMsg, ControlReceiver, ControlSender},
16    element::{Context, SourceElement},
17    error::Result,
18    graph::{GraphSnapshot, NodeInfo, PipelineGraph, log_topology},
19    playback_clock::PlaybackClock,
20};
21
22use super::{PipelineBuilder, builder::SourceEntry};
23
24/// Top-level pipeline: one or more sources (see [`PipelineBuilder`], with
25/// everything reachable from each source's own src pads already linked)
26/// plus the bus every source reports events on and the [`Clock`] every
27/// [`crate::elements::Pacer`] in it shares.
28///
29/// `run()` is asynchronous: it starts every source on its own background
30/// thread and returns immediately, rather than blocking the caller for the
31/// whole play-through. Returned as `Arc<Pipeline>` (that's what
32/// [`Pipeline::new`]/[`PipelineBuilder::build`] return) — the background
33/// threads deliberately do not retain an owning handle, so dropping the
34/// last external `Arc` can stop them. The `Arc` also lets [`Pipeline::pause`]/
35/// [`Pipeline::resume`]/[`Pipeline::stop`] be called from another thread
36/// while it's running.
37///
38/// There's no separate "is it done yet" query or callback: watch
39/// [`Pipeline::bus`] instead. [`BusReceiver::iter`]/
40/// [`BusReceiver::log_events`] block until every [`Bus`] sender has been
41/// dropped. Under the normal ownership path that happens once every
42/// source's background thread (and everything reachable from it) has
43/// fully finished, so draining the bus doubles as "wait for completion" —
44/// with more than one source, that means waiting for *all* of them, not
45/// just the first to reach `Eos`. A caller that clones the [`Context`]
46/// supplied to a source's own `wire` closure also retains its `Bus`
47/// sender; in that case bus draining intentionally remains blocked until
48/// that extra context is dropped. A source-level failure (returned from
49/// [`crate::element::SourceElement::run`] itself, as opposed to one
50/// reported from inside a `Queue`) shows up there too, as a
51/// [`BusEvent::Error`] under that source's own name, since there's no
52/// synchronous return path left to carry it.
53///
54/// A `Pipeline` isn't reusable once `run()` has been called (whether it
55/// finished via every source's natural `Eos`, [`Pipeline::finish`], or
56/// [`Pipeline::stop`]) — a
57/// second `run()` call is a no-op; build a fresh `Pipeline` for another
58/// play-through.
59pub struct Pipeline {
60    /// This pipeline's own id — passed to [`Pipeline::new`]/
61    /// [`PipelineBuilder::new`], stamped onto every source's own `pp_log`
62    /// there and onto every element that passes through a [`super::ChainBuilder`]
63    /// built with it (see [`Pipeline::id`]).
64    pub(super) id: Arc<str>,
65    /// Logging identity for pipeline-level topology records.
66    pub(super) pp_log: PpLog,
67    pub(super) sources: Mutex<Option<Vec<SourceEntry>>>,
68    /// Taken (leaving `None` behind) the moment `run()` starts, and cloned
69    /// once per source into that source's own background thread — so once
70    /// a pipeline is running, `Pipeline` itself no longer holds a `Bus`
71    /// sender directly. If it did, [`BusReceiver::iter`] could never
72    /// observe every sender dropped (one would always still be sitting
73    /// right here), and would block forever instead of unblocking once
74    /// every source actually finishes.
75    pub(super) bus: Mutex<Option<Bus>>,
76    /// One [`ControlSender`] per source, in the same order
77    /// [`PipelineBuilder::add_source`] was called — [`Pipeline::finish`]/
78    /// `stop`/`pause`/`resume`/`seek` send to every one of these in turn (each
79    /// `send` is its own synchronous rendezvous with that source's own
80    /// control cascade — see [`crate::control::ControlSender::send`] — so
81    /// this serializes across sources rather than fanning out in
82    /// parallel; fine for the handful of sources this is meant for).
83    pub(super) control_txs: Vec<ControlSender>,
84    /// Taken (leaving `None` behind) the moment `run()` starts, and moved
85    /// one per thread — same reasoning as `bus` above. If `Pipeline` kept
86    /// its own clone of each alive for its whole lifetime instead, that
87    /// control channel's receiver side would never fully disconnect even
88    /// after its thread has long since exited, so a
89    /// [`Pipeline::stop`]/`pause`/`resume` racing that thread's own
90    /// natural end (e.g. called right as it finishes on its own) could
91    /// enqueue a `Request` nobody will ever read *or drop* — leaving
92    /// [`crate::control::ControlSender::send`]'s rendezvous ack blocked
93    /// forever instead of unblocked by the disconnect, the way it is the
94    /// moment the *last* `ControlReceiver` clone actually goes away.
95    pub(super) control_rxs: Mutex<Option<Vec<ControlReceiver>>>,
96    pub(super) clock: Arc<Clock>,
97    pub(super) playback_clock: Arc<PlaybackClock>,
98    pub(super) bus_rx: BusReceiver,
99    /// How many source threads are still running — `0` before `run()` and
100    /// again once every source's thread has finished. `AtomicUsize` rather
101    /// than a per-source flag: every call site (`pause`/`resume`/`stop`/
102    /// `seek`) only ever needs "is anything still running at all", never
103    /// which specific source.
104    pub(super) running: Arc<AtomicUsize>,
105    /// Tracks whether `Pipeline::pause` has completed without a matching
106    /// resume. This cannot be inferred from `Clock`: pausing before the first
107    /// media timestamp leaves an unset clock unchanged while downstream
108    /// queues are nevertheless paused.
109    pub(super) paused: AtomicBool,
110    /// Handles for every source thread started by [`Pipeline::run`]. They
111    /// are retained so dropping the pipeline can synchronously stop and
112    /// join live sources instead of leaving detached work behind.
113    pub(super) workers: Mutex<Vec<JoinHandle<()>>>,
114    /// Live node/edge graph backing snapshots and topology rendering.
115    pub(super) graph: PipelineGraph,
116}
117
118impl Pipeline {
119    /// `id` names this pipeline — stamped into the source's own `pp_log` as
120    /// its `pipeline_id` right away, and folded into the [`Context`] handed
121    /// to `wire` (see [`super::ChainBuilder`]'s own docs).
122    ///
123    /// `wire` is called once with the freshly created source and a
124    /// [`Context`] bundling this pipeline's `Bus`, `id`, [`PipelineGraph`]
125    /// (already seeded with the source itself), and `Clock` (share it with
126    /// every [`crate::elements::Pacer`] via `Clock::clone` — one clock per
127    /// pipeline, so every paced branch agrees on the same t=0 and the same
128    /// pause/resume timeline) — everything a [`super::ChainBuilder`]/
129    /// [`crate::elements::Tee`] needs, in one `Arc` clone instead of four
130    /// separate arguments. `wire` creates detached chains and attaches
131    /// them through [`Context::attach`]. Pads left unattached drop data.
132    ///
133    /// The single-source special case of [`PipelineBuilder`] — see its own
134    /// docs for combining more than one live source (e.g. a video capture
135    /// and an audio capture) into one `Pipeline`.
136    pub fn new<S: SourceElement + 'static>(
137        id: impl Into<String>,
138        source: S,
139        wire: impl FnOnce(&mut S, &Arc<Context>) -> Result<()>,
140    ) -> Result<Arc<Self>> {
141        Ok(PipelineBuilder::new(id).add_source(source, wire)?.build())
142    }
143
144    /// This pipeline's own id, as passed to [`Pipeline::new`].
145    pub fn id(&self) -> &str {
146        &self.id
147    }
148
149    pub fn bus(&self) -> &BusReceiver {
150        &self.bus_rx
151    }
152
153    /// Returns a consistent node/edge snapshot of the live graph. Detached
154    /// branches do not appear; a successful attach or detach increments its
155    /// revision exactly once.
156    pub fn graph(&self) -> GraphSnapshot {
157        self.graph.snapshot()
158    }
159
160    pub fn elements(&self) -> Vec<NodeInfo> {
161        self.graph().nodes
162    }
163
164    /// Human-readable rundown of [`Pipeline::elements`]: one line per
165    /// branch — each element nothing else in the graph feeds into (a
166    /// terminal sink, or an empty [`crate::elements::Tee`] with no sinks
167    /// attached yet) — formatted `Type(name) - Type(name) - ...` by
168    /// walking that element's `upstream` chain back to the source.
169    /// Multiple branches (fan-out across more than one src pad, or a
170    /// `Tee`) are joined by newlines.
171    pub fn topology(&self) -> String {
172        self.graph().topology()
173    }
174
175    /// The clock every `Pacer` in this pipeline paces against — see
176    /// [`Pipeline::pause`] for why callers don't usually need to touch
177    /// this directly.
178    pub fn clock(&self) -> &Arc<Clock> {
179        &self.clock
180    }
181
182    /// Media-position clock shared by audio output and video scheduling.
183    pub fn playback_clock(&self) -> &Arc<PlaybackClock> {
184        &self.playback_clock
185    }
186
187    /// Starts driving the source on a background thread and returns
188    /// immediately — see the type-level docs for how to learn when it's
189    /// actually done. A no-op if this `Pipeline` is already running or
190    /// has already finished a previous run — this type has no "reset"
191    /// path; build a fresh `Pipeline` for another play-through.
192    pub fn run(&self) {
193        let Some(sources) = self.sources.lock().unwrap().take() else {
194            return;
195        };
196        // Always `Some` in lockstep with `sources` above — all three taken
197        // exactly once, on whichever `run()` call actually wins the
198        // `sources` guard.
199        let Some(bus) = self.bus.lock().unwrap().take() else {
200            return;
201        };
202        let Some(control_rxs) = self.control_rxs.lock().unwrap().take() else {
203            return;
204        };
205
206        if crate::log::enabled(crate::log::Level::Info) {
207            log_topology(&self.pp_log, "run", &self.graph());
208        }
209        self.running.store(sources.len(), Ordering::Release);
210        for ((source_id, source), control_rx) in sources.into_iter().zip(control_rxs) {
211            let bus = bus.for_element(source_id);
212            let running = Arc::clone(&self.running);
213            let handle = thread::Builder::new()
214                .name("pipeline:source".into())
215                .spawn(move || {
216                    // Keep these as locals in this order. During unwinding the
217                    // guard is dropped first, then the receiver, then the
218                    // source. That makes a Pipeline indirectly retained by a
219                    // custom source safe to drop from this worker thread.
220                    let mut source = source;
221                    let control_rx = control_rx;
222                    let _running = RunningSourceGuard::new(running);
223
224                    let source_name = source.name();
225                    let source_type = source.element_type();
226                    // `source.run()` itself already reports non-fatal,
227                    // per-buffer failures to `bus` as it goes (see
228                    // `SourceElement::run`'s docs) — a returned `Err` here
229                    // means something genuinely ended this source, e.g.
230                    // a `Seek` that failed outright.
231                    let outcome = if let Err(error) = source.run(&control_rx, &bus) {
232                        bus.post(
233                            source.pp_log(),
234                            BusEvent::Error {
235                                element_type: source_type,
236                                name: source_name.clone(),
237                                error,
238                            },
239                        );
240                        "error"
241                    } else {
242                        "ok"
243                    };
244                    pp_info!(pp_log: source.pp_log(), "finished outcome={outcome}");
245                })
246                .expect("failed to spawn pipeline source thread");
247            self.workers.lock().unwrap().push(handle);
248        }
249    }
250
251    /// Blocks until every element downstream of every source has paused —
252    /// see [`crate::control::drain_control`] (source side) and
253    /// [`crate::queue::Queue`]'s worker loop (each thread boundary). Also
254    /// pauses this pipeline's `Clock` before that synchronous cascade
255    /// starts, so time spent waiting for a busy downstream element to
256    /// acknowledge `Pause` is frozen too and a `Pacer` doesn't see a jump
257    /// once resumed. No-op if `run()` isn't currently in progress on
258    /// another thread.
259    pub fn pause(&self) {
260        if self.running.load(Ordering::Acquire) == 0 {
261            return;
262        }
263        let msg = ControlMsg::Pause;
264        pp_trace!(
265            pp_log: &self.pp_log,
266            "event=control control={msg:?} phase=requested"
267        );
268        self.clock.interrupt();
269        self.clock.pause();
270        self.paused.store(true, Ordering::Release);
271        for control_tx in &self.control_txs {
272            control_tx.send(msg);
273        }
274        pp_trace!(
275            pp_log: &self.pp_log,
276            "event=control control={msg:?} phase=completed outcome=ok"
277        );
278    }
279
280    /// Undoes [`Pipeline::pause`]. Resumes the `Clock` first, so it's
281    /// already shifted forward by the time `Pacer`s start receiving
282    /// frames again.
283    pub fn resume(&self) {
284        if self.running.load(Ordering::Acquire) == 0 {
285            return;
286        }
287        self.paused.store(false, Ordering::Release);
288        let msg = ControlMsg::Resume;
289        pp_trace!(
290            pp_log: &self.pp_log,
291            "event=control control={msg:?} phase=requested"
292        );
293        self.clock.resume();
294        for control_tx in &self.control_txs {
295            control_tx.send(msg);
296        }
297        pp_trace!(
298            pp_log: &self.pp_log,
299            "event=control control={msg:?} phase=completed outcome=ok"
300        );
301    }
302
303    /// Performs an early, full stop — abandons buffered work rather than
304    /// draining to a natural `Eos`. This call is synchronous: it sends
305    /// [`ControlMsg::Stop`] to every source in turn and waits for each
306    /// one's own cascade to finish before moving to the next — sequential,
307    /// not parallel, across sources (fine for the handful of sources this
308    /// is meant for). It therefore cannot preempt an arbitrary
309    /// source read or `Sink::consume` call already blocked inside user or
310    /// external-library code; the call returns only after that work gives
311    /// the control cascade a turn. After it returns, watch [`Pipeline::bus`]
312    /// for every source's background thread to finish. Not reusable
313    /// afterward — build a new `Pipeline` for the next play-through.
314    pub fn stop(&self) {
315        if self.running.load(Ordering::Acquire) == 0 {
316            return;
317        }
318        let msg = ControlMsg::Stop;
319        self.paused.store(false, Ordering::Release);
320        pp_trace!(
321            pp_log: &self.pp_log,
322            "event=control control={msg:?} phase=requested"
323        );
324        self.clock.interrupt();
325        for control_tx in &self.control_txs {
326            control_tx.send(msg);
327        }
328        pp_trace!(
329            pp_log: &self.pp_log,
330            "event=control control={msg:?} phase=completed outcome=ok"
331        );
332    }
333
334    /// Gracefully completes every source and waits for the whole graph to
335    /// drain. Each source stops producing and places `MediaBuffer::Eos` behind
336    /// its already-produced data; queues preserve that order, stateful codecs
337    /// flush delayed output, and muxers finalize only after their EOS arrives.
338    ///
339    /// Unlike [`Pipeline::stop`], this does not abandon queued work. If the
340    /// pipeline is paused, it resumes the control cascade first so a full
341    /// paused queue cannot prevent its ordered EOS from being enqueued. The
342    /// call returns only after every source thread (and the Queue workers each
343    /// source owns) has finished. The pipeline is not reusable afterward.
344    pub fn finish(&self) {
345        if self.running.load(Ordering::Acquire) == 0 {
346            self.join_workers();
347            return;
348        }
349
350        pp_trace!(
351            pp_log: &self.pp_log,
352            "event=finish phase=requested"
353        );
354        self.clock.interrupt();
355        if self.paused.load(Ordering::Acquire) {
356            self.resume();
357        }
358        for control_tx in &self.control_txs {
359            control_tx.finish();
360        }
361        self.join_workers();
362        pp_trace!(
363            pp_log: &self.pp_log,
364            "event=finish phase=completed outcome=ok"
365        );
366    }
367
368    fn join_workers(&self) {
369        let current_thread = thread::current().id();
370        let mut workers = self
371            .workers
372            .lock()
373            .unwrap_or_else(|poisoned| poisoned.into_inner());
374        for worker in workers.drain(..) {
375            if worker.thread().id() != current_thread {
376                let _ = worker.join();
377            }
378        }
379    }
380
381    /// Jumps to an absolute position from the start of the media. Blocks
382    /// until every source has repositioned (see
383    /// [`crate::element::SourceElement::seek`]) and every element
384    /// downstream of each has reacted (a `Queue` drops its stale backlog, a
385    /// decoder flushes, a `Pacer` re-anchors both its pts reference and
386    /// this pipeline's `Clock`) — same synchronous cascade as `pause`/
387    /// `resume`/`stop`. One-shot, unlike `pause`: nothing further to undo
388    /// afterward, playback just continues from the new position. No-op
389    /// if `run()` isn't currently in progress on another thread.
390    ///
391    /// Signals the clock's interrupt epoch before starting the synchronous
392    /// cascade so a `Pacer` in a long wait can return its worker promptly.
393    /// The clock's playback anchor is still reset later, inside
394    /// [`Sink::control`](crate::element::Sink::control) on `Pacer`, after
395    /// that in-flight frame is
396    /// out of the way.
397    ///
398    /// A source that doesn't support seeking (e.g. a live capture) reports
399    /// that via its own [`crate::element::SourceElement::seek`] returning
400    /// an error — surfaced on [`Pipeline::bus`] as a
401    /// [`BusEvent::Error`] under that source's name, same as any other
402    /// per-source failure, rather than failing this call outright or
403    /// skipping that source silently.
404    pub fn seek(&self, target: Duration) {
405        if self.running.load(Ordering::Acquire) == 0 {
406            return;
407        }
408        let msg = ControlMsg::Seek(target);
409        pp_trace!(
410            pp_log: &self.pp_log,
411            "event=control control={msg:?} phase=requested"
412        );
413        self.clock.interrupt();
414        self.playback_clock.reset_for_seek();
415        for control_tx in &self.control_txs {
416            control_tx.send(msg);
417        }
418        pp_trace!(
419            pp_log: &self.pp_log,
420            "event=control control={msg:?} phase=completed outcome=ok"
421        );
422    }
423}
424
425/// Decrements the live-source count even if a source panics while running.
426struct RunningSourceGuard {
427    running: Arc<AtomicUsize>,
428}
429
430impl RunningSourceGuard {
431    fn new(running: Arc<AtomicUsize>) -> Self {
432        Self { running }
433    }
434}
435
436impl Drop for RunningSourceGuard {
437    fn drop(&mut self) {
438        self.running.fetch_sub(1, Ordering::AcqRel);
439    }
440}
441
442impl Drop for Pipeline {
443    fn drop(&mut self) {
444        // Send Stop while every sender is still alive. Merely dropping the
445        // senders would not wake a source polling an empty control channel.
446        self.stop();
447
448        self.join_workers();
449    }
450}