Skip to main content

media_pp/core/pipeline/
chain.rs

1use std::sync::Arc;
2
3use crate::pp_log::{PpLog, pp_trace};
4
5use crate::{
6    buffer::MediaBuffer,
7    bus::{Bus, BusEvent},
8    control::ControlMsg,
9    element::{Context, Element, ElementType, Filter, Sink, Source, element_pp_log},
10    error::Result,
11    graph::{BranchId, BranchPlan, ElementId, GraphError, NodeInfo, PlannedEdge, PortRef},
12    pad::SrcPad,
13    queue::{OverflowPolicy, Queue},
14};
15
16/// Builds one chain segment (a run of elements that all execute on the same
17/// thread). Call [`ChainBuilder::queue`] to close the current segment behind
18/// a `Queue` and start a new one on its own worker thread.
19///
20/// Because each element needs a handle to *its* downstream to be
21/// constructed, the chain is assembled back-to-front: elements are
22/// collected in call order, then folded right-to-left starting from the
23/// terminal `Sink` at [`ChainBuilder::to`] time.
24pub struct ChainBuilder {
25    context: Arc<Context>,
26    elements: Vec<Box<dyn StageBuilder>>,
27    /// Nodes kept locally until this builder becomes a `DetachedBranch`
28    /// and an attach operation commits the complete plan.
29    planned: Vec<PlannedNode>,
30    error: Option<GraphError>,
31}
32
33struct PlannedNode {
34    info: NodeInfo,
35    output_port: Arc<str>,
36}
37
38/// A fully constructed runtime chain whose graph nodes are still detached.
39/// Dropping it has no topology effect; only an attach operation commits it.
40pub struct DetachedBranch {
41    pub(crate) root: Box<dyn Sink>,
42    pub(crate) plan: BranchPlan,
43}
44
45impl DetachedBranch {
46    /// Returns the stable graph identity of the first element in this branch.
47    ///
48    /// The ID is reserved during construction but does not appear in the live
49    /// graph until a successful [`Context::attach`].
50    pub fn root_id(&self) -> ElementId {
51        self.plan.root
52    }
53}
54
55trait StageBuilder: Send {
56    fn wrap(
57        self: Box<Self>,
58        downstream: Box<dyn Sink>,
59        bus: &Bus,
60        pipeline_id: &str,
61    ) -> Result<Box<dyn Sink>>;
62}
63
64struct DirectStage<T>(T);
65
66/// Adds uniform EOS/control boundary tracing to every direct filter without
67/// requiring each built-in or downstream custom element to duplicate it.
68struct FlowTracer<T> {
69    inner: T,
70}
71
72impl<T: Element> Element for FlowTracer<T> {
73    fn name(&self) -> Arc<str> {
74        self.inner.name()
75    }
76
77    fn element_type(&self) -> ElementType {
78        self.inner.element_type()
79    }
80
81    fn graph_id(&self) -> Option<ElementId> {
82        self.inner.graph_id()
83    }
84
85    fn pp_log(&self) -> &PpLog {
86        self.inner.pp_log()
87    }
88
89    fn pp_log_mut(&mut self) -> &mut PpLog {
90        self.inner.pp_log_mut()
91    }
92}
93
94impl<T: Source> Source for FlowTracer<T> {
95    fn src_pads(&mut self) -> &mut [SrcPad] {
96        self.inner.src_pads()
97    }
98}
99
100impl<T: Sink> Sink for FlowTracer<T> {
101    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
102        let is_eos = buf.is_eos();
103        if is_eos {
104            pp_trace!(pp_log: self.inner.pp_log(), "event=eos phase=received");
105        }
106        let result = self.inner.consume(buf);
107        if is_eos {
108            match &result {
109                Ok(()) => pp_trace!(
110                    pp_log: self.inner.pp_log(),
111                    "event=eos phase=completed outcome=ok"
112                ),
113                Err(error) => pp_trace!(
114                    pp_log: self.inner.pp_log(),
115                    "event=eos phase=completed outcome=error error={error}"
116                ),
117            }
118        }
119        result
120    }
121
122    fn control(&mut self, msg: ControlMsg) -> Result<()> {
123        pp_trace!(
124            pp_log: self.inner.pp_log(),
125            "event=control control={msg:?} phase=received"
126        );
127        let result = self.inner.control(msg);
128        match &result {
129            Ok(()) => pp_trace!(
130                pp_log: self.inner.pp_log(),
131                "event=control control={msg:?} phase=completed outcome=ok"
132            ),
133            Err(error) => pp_trace!(
134                pp_log: self.inner.pp_log(),
135                "event=control control={msg:?} phase=completed outcome=error error={error}"
136            ),
137        }
138        result
139    }
140}
141
142impl<T> StageBuilder for DirectStage<T>
143where
144    T: Filter + 'static,
145{
146    fn wrap(
147        self: Box<Self>,
148        downstream: Box<dyn Sink>,
149        _bus: &Bus,
150        pipeline_id: &str,
151    ) -> Result<Box<dyn Sink>> {
152        let mut element = self.0;
153        *element.pp_log_mut() =
154            element_pp_log(element.element_type(), &element.name(), Some(pipeline_id));
155        element.src_pads()[0].link(downstream);
156        Ok(Box::new(FlowTracer { inner: element }))
157    }
158}
159
160struct QueueStage {
161    id: ElementId,
162    name: String,
163    capacity: usize,
164    policy: OverflowPolicy,
165}
166
167/// Traces EOS/control at a terminal `Sink` and posts a `BusEvent::Eos` (under
168/// the sink's own `Element::name()`) once EOS completes — mirrors what
169/// `Queue` does for its own downstream, but without introducing a thread
170/// boundary. This is what lets a fully direct chain (no `queue()` calls at
171/// all) still report EOS on the bus.
172struct TerminalTracer {
173    bus: Bus,
174    inner: Box<dyn Sink>,
175}
176
177impl Element for TerminalTracer {
178    fn name(&self) -> Arc<str> {
179        self.inner.name()
180    }
181
182    fn element_type(&self) -> ElementType {
183        self.inner.element_type()
184    }
185
186    fn graph_id(&self) -> Option<ElementId> {
187        self.inner.graph_id()
188    }
189
190    fn pp_log(&self) -> &PpLog {
191        self.inner.pp_log()
192    }
193
194    fn pp_log_mut(&mut self) -> &mut PpLog {
195        self.inner.pp_log_mut()
196    }
197}
198
199impl Sink for TerminalTracer {
200    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
201        let is_eos = buf.is_eos();
202        if is_eos {
203            pp_trace!(pp_log: self.inner.pp_log(), "event=eos phase=received");
204        }
205        let result = self.inner.consume(buf);
206        if is_eos {
207            match &result {
208                Ok(()) => {
209                    pp_trace!(
210                        pp_log: self.inner.pp_log(),
211                        "event=eos phase=completed outcome=ok"
212                    );
213                    self.bus.post(
214                        self.inner.pp_log(),
215                        BusEvent::Eos {
216                            element_type: self.inner.element_type(),
217                            name: self.inner.name(),
218                        },
219                    );
220                }
221                Err(error) => pp_trace!(
222                    pp_log: self.inner.pp_log(),
223                    "event=eos phase=completed outcome=error error={error}"
224                ),
225            }
226        }
227        result
228    }
229
230    fn control(&mut self, msg: ControlMsg) -> Result<()> {
231        pp_trace!(
232            pp_log: self.inner.pp_log(),
233            "event=control control={msg:?} phase=received"
234        );
235        let result = self.inner.control(msg);
236        match &result {
237            Ok(()) => pp_trace!(
238                pp_log: self.inner.pp_log(),
239                "event=control control={msg:?} phase=completed outcome=ok"
240            ),
241            Err(error) => pp_trace!(
242                pp_log: self.inner.pp_log(),
243                "event=control control={msg:?} phase=completed outcome=error error={error}"
244            ),
245        }
246        result
247    }
248}
249
250impl StageBuilder for QueueStage {
251    fn wrap(
252        self: Box<Self>,
253        downstream: Box<dyn Sink>,
254        bus: &Bus,
255        pipeline_id: &str,
256    ) -> Result<Box<dyn Sink>> {
257        Ok(Box::new(Queue::spawn_with_policy(
258            self.name,
259            self.capacity,
260            downstream,
261            bus.for_element(self.id),
262            self.policy,
263            Some(pipeline_id),
264        )?))
265    }
266}
267
268impl ChainBuilder {
269    /// Starts a detached branch plan. Prefer [`Context::branch`] at call
270    /// sites; it makes the owning pipeline explicit without cloning the
271    /// context manually.
272    pub fn new(context: Arc<Context>) -> Self {
273        Self {
274            context,
275            elements: Vec::new(),
276            planned: Vec::new(),
277            error: None,
278        }
279    }
280
281    /// Adds a single-output `Filter` (decoder, encoder, filter, ...) that
282    /// receives via `Sink` and produces through its own (single) src pad.
283    /// It runs on the same thread as whatever is upstream of it — direct
284    /// function call, no queue.
285    pub fn pipe<T: Filter + 'static>(mut self, mut element: T) -> Self {
286        let name = element.name();
287        let pad_count = element.src_pads().len();
288        if pad_count != 1 && self.error.is_none() {
289            self.error = Some(GraphError::NotSingleOutput {
290                name: name.clone(),
291                count: pad_count,
292            });
293        }
294        let output_port = element
295            .src_pads()
296            .first()
297            .map(|pad| Arc::<str>::from(pad.name()))
298            .unwrap_or_else(|| "src".into());
299        self.planned.push(PlannedNode {
300            info: NodeInfo {
301                id: self.context.graph.reserve_element_id(),
302                element_type: element.element_type(),
303                name,
304            },
305            output_port,
306        });
307        self.elements.push(Box::new(DirectStage(element)));
308        self
309    }
310
311    /// Introduces a thread boundary (blocking when full — see
312    /// [`OverflowPolicy::Block`]): everything added after this runs on its
313    /// own worker thread instead of the thread that feeds this queue.
314    pub fn queue(self, name: impl Into<String>, capacity: usize) -> Self {
315        self.queue_with_policy(name, capacity, OverflowPolicy::default())
316    }
317
318    /// Same as [`ChainBuilder::queue`], but lets you choose what happens
319    /// when the queue is full (e.g. [`OverflowPolicy::DropNewest`] for a
320    /// live source that shouldn't stall upstream).
321    pub fn queue_with_policy(
322        mut self,
323        name: impl Into<String>,
324        capacity: usize,
325        policy: OverflowPolicy,
326    ) -> Self {
327        let name: Arc<str> = name.into().into();
328        let id = self.context.graph.reserve_element_id();
329        self.planned.push(PlannedNode {
330            info: NodeInfo {
331                id,
332                element_type: ElementType::Queue,
333                name: name.clone(),
334            },
335            output_port: format!("{name}_src").into(),
336        });
337        self.elements.push(Box::new(QueueStage {
338            id,
339            name: name.to_string(),
340            capacity,
341            policy,
342        }));
343        self
344    }
345
346    /// Terminates the chain with a `Sink` (muxer, file sink, ...) and
347    /// assembles everything into a single `Box<dyn Sink>` ready to be
348    /// linked into a source's src pad. The terminal's own `Element::name()`
349    /// is what shows up on the bus when it reports EOS.
350    pub fn to(self, mut terminal: Box<dyn Sink>) -> Result<DetachedBranch> {
351        if let Some(error) = self.error {
352            return Err(error.into());
353        }
354        *terminal.pp_log_mut() = element_pp_log(
355            terminal.element_type(),
356            &terminal.name(),
357            Some(&self.context.pipeline_id),
358        );
359        let terminal_info = NodeInfo {
360            id: terminal
361                .graph_id()
362                .unwrap_or_else(|| self.context.graph.reserve_element_id()),
363            element_type: terminal.element_type(),
364            name: terminal.name(),
365        };
366        let terminal_id = terminal_info.id;
367        let mut nodes: Vec<_> = self.planned.iter().map(|node| node.info.clone()).collect();
368        nodes.push(terminal_info);
369        let edges = nodes
370            .windows(2)
371            .enumerate()
372            .map(|(index, pair)| PlannedEdge {
373                from: PortRef {
374                    element: pair[0].id,
375                    port: self.planned[index].output_port.clone(),
376                },
377                to: PortRef {
378                    element: pair[1].id,
379                    port: "sink".into(),
380                },
381            })
382            .collect();
383        let root_id = nodes.first().expect("terminal always supplies one node").id;
384        let terminal: Box<dyn Sink> = Box::new(TerminalTracer {
385            bus: self.context.bus.for_element(terminal_id),
386            inner: terminal,
387        });
388        let root = self
389            .elements
390            .into_iter()
391            .rev()
392            .try_fold(terminal, |downstream, stage| {
393                stage.wrap(downstream, &self.context.bus, &self.context.pipeline_id)
394            })?;
395        Ok(DetachedBranch {
396            root,
397            plan: BranchPlan {
398                nodes,
399                edges,
400                root: root_id,
401            },
402        })
403    }
404
405    /// Alias of [`Self::to`] retained for callers that prefer builder-style
406    /// terminology when supplying the terminal sink.
407    pub fn build(self, terminal: Box<dyn Sink>) -> Result<DetachedBranch> {
408        self.to(terminal)
409    }
410}
411
412impl Context {
413    /// Starts a detached branch plan scoped to this source and pipeline.
414    ///
415    /// Building the branch allocates its runtime elements but does not publish
416    /// them in the graph until [`Self::attach`] succeeds.
417    pub fn branch(self: &Arc<Self>) -> ChainBuilder {
418        ChainBuilder::new(self.clone())
419    }
420
421    /// Atomically attaches a completed branch to one source pad.
422    ///
423    /// Returns the stable branch identity used by later dynamic graph
424    /// operations. An invalid index or already-linked pad returns a
425    /// [`GraphError`] without changing either the runtime connection or graph.
426    pub fn attach<S: Source>(
427        &self,
428        source: &mut S,
429        pad_index: usize,
430        branch: DetachedBranch,
431    ) -> Result<BranchId> {
432        let pads = source.src_pads();
433        let pad_count = pads.len();
434        let pad = pads.get_mut(pad_index).ok_or(GraphError::PadOutOfRange {
435            index: pad_index,
436            pad_count,
437        })?;
438        self.attach_pad(pad, branch)
439    }
440
441    pub(crate) fn attach_pad(&self, pad: &mut SrcPad, branch: DetachedBranch) -> Result<BranchId> {
442        if pad.is_linked() {
443            return Err(GraphError::PadAlreadyLinked(pad.name().to_owned()).into());
444        }
445        let from_port: Arc<str> = pad.name().into();
446        let DetachedBranch { root, plan } = branch;
447        Ok(self
448            .graph
449            .attach_with(self.source_id, from_port, plan, |_| {
450                pad.link(root);
451                Ok(())
452            })?)
453    }
454}