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}