Skip to main content

media_pp/core/pipeline/
chain.rs

1use std::{collections::HashMap, sync::Arc};
2
3use crate::pp_log::{PpLog, pp_trace};
4
5use crate::{
6    buffer::MediaBuffer,
7    bus::{Bus, BusEvent},
8    contract::{InputContract, OutputContract},
9    control::ControlMsg,
10    element::{Context, Element, ElementType, Filter, Sink, Source, element_pp_log},
11    error::Result,
12    graph::{
13        BranchId, BranchPlan, ElementId, GraphError, Incoming, NodeInfo, PlannedEdge,
14        PortContracts, PortRef, ResolvedFlow,
15    },
16    pad::SrcPad,
17    queue::{OverflowPolicy, Queue},
18};
19
20/// Builds one chain segment (a run of elements that all execute on the same
21/// thread). Call [`ChainBuilder::queue`] to close the current segment behind
22/// a `Queue` and start a new one on its own worker thread.
23///
24/// Because each element needs a handle to *its* downstream to be
25/// constructed, the chain is assembled back-to-front: elements are
26/// collected in call order, then folded right-to-left starting from the
27/// terminal `Sink` at [`ChainBuilder::to`] time.
28pub struct ChainBuilder {
29    context: Arc<Context>,
30    elements: Vec<Box<dyn StageBuilder>>,
31    /// Nodes kept locally until this builder becomes a `DetachedBranch`
32    /// and an attach operation commits the complete plan.
33    planned: Vec<PlannedNode>,
34    error: Option<GraphError>,
35}
36
37struct PlannedNode {
38    info: NodeInfo,
39    output_port: Arc<str>,
40    input: InputContract,
41    output: OutputContract,
42}
43
44/// A fully constructed runtime chain whose graph nodes are still detached.
45/// Dropping it has no topology effect; only an attach operation commits it.
46pub struct DetachedBranch {
47    pub(crate) root: Box<dyn Sink>,
48    pub(crate) plan: BranchPlan,
49}
50
51impl DetachedBranch {
52    /// Returns the stable graph identity of the first element in this branch.
53    ///
54    /// The ID is reserved during construction but does not appear in the live
55    /// graph until a successful [`Context::attach`].
56    pub fn root_id(&self) -> ElementId {
57        self.plan.root
58    }
59}
60
61trait StageBuilder: Send {
62    /// Turns one planned stage into a live `Sink` in front of `downstream`.
63    ///
64    /// Takes the whole `Context` rather than the pieces it needs, because
65    /// what a stage needs from its pipeline has grown twice already — the
66    /// bus, the id, and now the clocks — and each addition changed every
67    /// implementation. One handle is also what makes it impossible to build
68    /// a stage against one pipeline's identity and another's timing.
69    fn wrap(
70        self: Box<Self>,
71        downstream: Box<dyn Sink>,
72        context: &Arc<Context>,
73    ) -> Result<Box<dyn Sink>>;
74}
75
76struct DirectStage<T>(T);
77
78/// Adds uniform EOS/control boundary tracing to every direct filter without
79/// requiring each built-in or downstream custom element to duplicate it.
80struct FlowTracer<T> {
81    inner: T,
82}
83
84impl<T: Element> Element for FlowTracer<T> {
85    fn name(&self) -> Arc<str> {
86        self.inner.name()
87    }
88
89    fn element_type(&self) -> ElementType {
90        self.inner.element_type()
91    }
92
93    fn graph_id(&self) -> Option<ElementId> {
94        self.inner.graph_id()
95    }
96
97    fn pp_log(&self) -> &PpLog {
98        self.inner.pp_log()
99    }
100
101    fn pp_log_mut(&mut self) -> &mut PpLog {
102        self.inner.pp_log_mut()
103    }
104}
105
106impl<T: Source> Source for FlowTracer<T> {
107    fn src_pads(&mut self) -> &mut [SrcPad] {
108        self.inner.src_pads()
109    }
110}
111
112impl<T: Filter> Sink for FlowTracer<T> {
113    fn ready_consume(&mut self) -> bool {
114        self.inner.ready_consume() && self.inner.src_pads().iter_mut().all(SrcPad::ready_consume)
115    }
116
117    fn input_contract(&self) -> InputContract {
118        self.inner.input_contract()
119    }
120
121    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
122        let is_eos = buf.is_eos();
123        if is_eos {
124            pp_trace!(pp_log: self.inner.pp_log(), "event=eos phase=received");
125        }
126        let result = self.inner.consume(buf);
127        if is_eos {
128            match &result {
129                Ok(()) => pp_trace!(
130                    pp_log: self.inner.pp_log(),
131                    "event=eos phase=completed outcome=ok"
132                ),
133                Err(error) => pp_trace!(
134                    pp_log: self.inner.pp_log(),
135                    "event=eos phase=completed outcome=error error={error}"
136                ),
137            }
138        }
139        result
140    }
141
142    fn control(&mut self, msg: ControlMsg) -> Result<()> {
143        pp_trace!(
144            pp_log: self.inner.pp_log(),
145            "event=control control={msg:?} phase=received"
146        );
147        let result = self.inner.control(msg.clone());
148        match &result {
149            Ok(()) => pp_trace!(
150                pp_log: self.inner.pp_log(),
151                "event=control control={msg:?} phase=completed outcome=ok"
152            ),
153            Err(error) => pp_trace!(
154                pp_log: self.inner.pp_log(),
155                "event=control control={msg:?} phase=completed outcome=error error={error}"
156            ),
157        }
158        result
159    }
160}
161
162impl<T> StageBuilder for DirectStage<T>
163where
164    T: Filter + 'static,
165{
166    fn wrap(
167        self: Box<Self>,
168        downstream: Box<dyn Sink>,
169        context: &Arc<Context>,
170    ) -> Result<Box<dyn Sink>> {
171        let mut element = self.0;
172        *element.pp_log_mut() = element_pp_log(
173            element.element_type(),
174            &element.name(),
175            Some(&context.pipeline_id),
176        );
177        element.attach_context(context);
178        element.src_pads()[0].link(downstream);
179        Ok(Box::new(FlowTracer { inner: element }))
180    }
181}
182
183struct QueueStage {
184    id: ElementId,
185    name: String,
186    capacity: usize,
187    policy: OverflowPolicy,
188}
189
190/// Traces EOS/control at a terminal `Sink` and posts a `BusEvent::Eos` (under
191/// the sink's own `Element::name()`) once EOS completes — mirrors what
192/// `Queue` does for its own downstream, but without introducing a thread
193/// boundary. This is what lets a fully direct chain (no `queue()` calls at
194/// all) still report EOS on the bus. During preroll it marks a terminal ready
195/// only after the wrapped sink's synchronous `consume` has returned `Ok`:
196/// accepted/submitted, not necessarily scanned out, played, or remotely
197/// received.
198struct TerminalTracer {
199    bus: Bus,
200    id: ElementId,
201    inner: Box<dyn Sink>,
202    paused: bool,
203    preroll: Option<Arc<crate::control::PrerollContext>>,
204}
205
206impl Element for TerminalTracer {
207    fn name(&self) -> Arc<str> {
208        self.inner.name()
209    }
210
211    fn element_type(&self) -> ElementType {
212        self.inner.element_type()
213    }
214
215    fn graph_id(&self) -> Option<ElementId> {
216        self.inner.graph_id()
217    }
218
219    fn pp_log(&self) -> &PpLog {
220        self.inner.pp_log()
221    }
222
223    fn pp_log_mut(&mut self) -> &mut PpLog {
224        self.inner.pp_log_mut()
225    }
226}
227
228impl Sink for TerminalTracer {
229    fn ready_consume(&mut self) -> bool {
230        if self.paused {
231            return false;
232        }
233        // This terminal's own readiness, not the whole preroll's. Closing only
234        // once every branch is done would let whichever reached the target
235        // first keep consuming for as long as the slowest one takes, leaving
236        // the streams at different positions when preroll finally completes.
237        if self
238            .preroll
239            .as_ref()
240            .is_some_and(|context| context.is_ready(self.id))
241        {
242            return false;
243        }
244        self.inner.ready_consume()
245    }
246
247    fn input_contract(&self) -> InputContract {
248        self.inner.input_contract()
249    }
250
251    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
252        let is_eos = buf.is_eos();
253        if is_eos {
254            pp_trace!(pp_log: self.inner.pp_log(), "event=eos phase=received");
255        }
256        let result = self.inner.consume(buf);
257        // `Sink::consume` defines successful terminal return as acceptance
258        // into that sink's output path. This is the preroll completion point;
259        // deliberately do not claim physical presentation has completed.
260        if result.is_ok()
261            && let Some(context) = &self.preroll
262        {
263            if is_eos {
264                context.mark_eos(self.id);
265            } else {
266                context.mark_ready(self.id);
267            }
268        }
269        if is_eos {
270            match &result {
271                Ok(()) => {
272                    pp_trace!(
273                        pp_log: self.inner.pp_log(),
274                        "event=eos phase=completed outcome=ok"
275                    );
276                    self.bus.post(
277                        self.inner.pp_log(),
278                        BusEvent::Eos {
279                            element_type: self.inner.element_type(),
280                            name: self.inner.name(),
281                        },
282                    );
283                }
284                Err(error) => pp_trace!(
285                    pp_log: self.inner.pp_log(),
286                    "event=eos phase=completed outcome=error error={error}"
287                ),
288            }
289        }
290        result
291    }
292
293    fn control(&mut self, msg: ControlMsg) -> Result<()> {
294        pp_trace!(
295            pp_log: self.inner.pp_log(),
296            "event=control control={msg:?} phase=received"
297        );
298        let result = self.inner.control(msg.clone());
299        if result.is_ok() {
300            match &msg {
301                ControlMsg::Pause => {
302                    self.paused = true;
303                    self.preroll = None;
304                }
305                ControlMsg::Resume => {
306                    self.paused = false;
307                    self.preroll = None;
308                }
309                ControlMsg::Preroll(context) => {
310                    self.paused = false;
311                    self.preroll = Some(Arc::clone(context));
312                }
313                ControlMsg::Stop => {
314                    if let Some(context) = self.preroll.take() {
315                        context.cancel();
316                    }
317                    self.paused = true;
318                }
319                ControlMsg::Flush | ControlMsg::CheckSeek(_) | ControlMsg::Seek(_) => {}
320            }
321        }
322        match &result {
323            Ok(()) => pp_trace!(
324                pp_log: self.inner.pp_log(),
325                "event=control control={msg:?} phase=completed outcome=ok"
326            ),
327            Err(error) => pp_trace!(
328                pp_log: self.inner.pp_log(),
329                "event=control control={msg:?} phase=completed outcome=error error={error}"
330            ),
331        }
332        result
333    }
334}
335
336impl StageBuilder for QueueStage {
337    fn wrap(
338        self: Box<Self>,
339        downstream: Box<dyn Sink>,
340        context: &Arc<Context>,
341    ) -> Result<Box<dyn Sink>> {
342        // No `attach_context`: a `Queue` is built here rather than handed in,
343        // so there is no chance of it having been given another pipeline's
344        // anything.
345        Ok(Box::new(Queue::spawn_with_policy(
346            self.name,
347            self.capacity,
348            downstream,
349            context.bus.for_element(self.id),
350            self.policy,
351            Some(&context.pipeline_id),
352        )?))
353    }
354}
355
356impl ChainBuilder {
357    /// Starts a detached branch plan. Prefer [`Context::branch`] at call
358    /// sites; it makes the owning pipeline explicit without cloning the
359    /// context manually.
360    pub fn new(context: Arc<Context>) -> Self {
361        Self {
362            context,
363            elements: Vec::new(),
364            planned: Vec::new(),
365            error: None,
366        }
367    }
368
369    /// Adds a single-output `Filter` (decoder, encoder, filter, ...) that
370    /// receives via `Sink` and produces through its own (single) src pad.
371    /// It runs on the same thread as whatever is upstream of it — direct
372    /// function call, no queue.
373    pub fn pipe<T: Filter + 'static>(mut self, mut element: T) -> Self {
374        let name = element.name();
375        let pad_count = element.src_pads().len();
376        if pad_count != 1 && self.error.is_none() {
377            self.error = Some(GraphError::NotSingleOutput {
378                name: name.clone(),
379                count: pad_count,
380            });
381        }
382        let (output_port, output) = element
383            .src_pads()
384            .first()
385            .map(|pad| (Arc::<str>::from(pad.name()), pad.contract()))
386            .unwrap_or_else(|| ("src".into(), OutputContract::Unknown));
387        self.planned.push(PlannedNode {
388            info: NodeInfo {
389                id: self.context.graph.reserve_element_id(),
390                element_type: element.element_type(),
391                name,
392            },
393            output_port,
394            input: element.input_contract(),
395            output,
396        });
397        self.elements.push(Box::new(DirectStage(element)));
398        self
399    }
400
401    /// Introduces a thread boundary (blocking when full — see
402    /// [`OverflowPolicy::Block`]): everything added after this runs on its
403    /// own worker thread instead of the thread that feeds this queue.
404    pub fn queue(self, name: impl Into<String>, capacity: usize) -> Self {
405        self.queue_with_policy(name, capacity, OverflowPolicy::default())
406    }
407
408    /// Same as [`ChainBuilder::queue`], but lets you choose what happens
409    /// when the queue is full (e.g. [`OverflowPolicy::DropNewest`] for a
410    /// live source that shouldn't stall upstream).
411    pub fn queue_with_policy(
412        mut self,
413        name: impl Into<String>,
414        capacity: usize,
415        policy: OverflowPolicy,
416    ) -> Self {
417        let name: Arc<str> = name.into().into();
418        let id = self.context.graph.reserve_element_id();
419        self.planned.push(PlannedNode {
420            info: NodeInfo {
421                id,
422                element_type: ElementType::Queue,
423                name: name.clone(),
424            },
425            output_port: format!("{name}_src").into(),
426            // A Queue is a thread boundary, not a transform: it hands
427            // downstream exactly what it was given. Declared here rather
428            // than on the element because a Queue owns its downstream
429            // sink directly and so has no pad to carry it.
430            input: InputContract::Any,
431            output: OutputContract::Passthrough,
432        });
433        self.elements.push(Box::new(QueueStage {
434            id,
435            name: name.to_string(),
436            capacity,
437            policy,
438        }));
439        self
440    }
441
442    /// Terminates the chain with a `Sink` (muxer, file sink, ...) and
443    /// assembles everything into a single `Box<dyn Sink>` ready to be
444    /// linked into a source's src pad. The terminal's own `Element::name()`
445    /// is what shows up on the bus when it reports EOS.
446    pub fn to(self, mut terminal: Box<dyn Sink>) -> Result<DetachedBranch> {
447        if let Some(error) = self.error {
448            return Err(error.into());
449        }
450        *terminal.pp_log_mut() = element_pp_log(
451            terminal.element_type(),
452            &terminal.name(),
453            Some(&self.context.pipeline_id),
454        );
455        terminal.attach_context(&self.context);
456        let terminal_info = NodeInfo {
457            id: terminal
458                .graph_id()
459                .unwrap_or_else(|| self.context.graph.reserve_element_id()),
460            element_type: terminal.element_type(),
461            name: terminal.name(),
462        };
463        let terminal_id = terminal_info.id;
464
465        // The terminal has no src pad, so nothing flows onward from it.
466        let mut contracts: HashMap<_, _> = self
467            .planned
468            .iter()
469            .map(|node| {
470                (
471                    node.info.id,
472                    PortContracts {
473                        input: node.input,
474                        output: node.output,
475                    },
476                )
477            })
478            .collect();
479        contracts.insert(
480            terminal_id,
481            PortContracts {
482                input: terminal.input_contract(),
483                output: OutputContract::Unknown,
484            },
485        );
486
487        let mut nodes: Vec<_> = self.planned.iter().map(|node| node.info.clone()).collect();
488        nodes.push(terminal_info);
489        let edges = nodes
490            .windows(2)
491            .enumerate()
492            .map(|(index, pair)| PlannedEdge {
493                from: PortRef {
494                    element: pair[0].id,
495                    port: self.planned[index].output_port.clone(),
496                },
497                to: PortRef {
498                    element: pair[1].id,
499                    port: "sink".into(),
500                },
501            })
502            .collect();
503        let root_id = nodes.first().expect("terminal always supplies one node").id;
504        let terminal: Box<dyn Sink> = Box::new(TerminalTracer {
505            bus: self.context.bus.for_element(terminal_id),
506            id: terminal_id,
507            inner: terminal,
508            paused: false,
509            preroll: None,
510        });
511        let plan = BranchPlan {
512            nodes,
513            edges,
514            contracts,
515            root: root_id,
516        };
517        // Nothing is flowing into a detached branch yet, so this only
518        // catches links downstream of a stage that produces something of
519        // its own — a decoder feeding a muxer, say. The same walk runs
520        // again at attach time with the real upstream contract.
521        plan.resolve(None)?;
522
523        let root = self
524            .elements
525            .into_iter()
526            .rev()
527            .try_fold(terminal, |downstream, stage| {
528                stage.wrap(downstream, &self.context)
529            })?;
530        Ok(DetachedBranch { root, plan })
531    }
532
533    /// Ends the chain at an already-assembled [`DetachedBranch`] — a
534    /// [`crate::elements::TeeBuilder`]'s fan-out, in practice — instead of a
535    /// plain `Sink`.
536    ///
537    /// A `Tee` cannot be a [`Self::pipe`] stage: its outputs live behind a
538    /// lock rather than in `src_pads`, so it is not a `Source` and nothing
539    /// can be chained onto it. It is still where a chain *ends*, though, and
540    /// without this the only way to put stages in front of one is to attach
541    /// the fan-out to a mid-chain element's own pad and then attach that
542    /// element separately.
543    ///
544    /// That wires the buffers correctly but misrecords the topology.
545    /// [`Context::attach`] always names the pipeline's source as the parent
546    /// node, because the element whose pad it was handed is not in the graph
547    /// yet — there is no other id it could use. The fan-out then renders as
548    /// the source's own, leaving an element it never passed through.
549    ///
550    /// Joining the two plans here keeps the edge on the stage that really
551    /// feeds the branch, so one attach commits the whole subgraph and the
552    /// diagram shows the fan-out where it occurs.
553    pub fn to_branch(self, downstream: DetachedBranch) -> Result<DetachedBranch> {
554        if let Some(error) = self.error {
555            return Err(error.into());
556        }
557        let DetachedBranch {
558            root: sink,
559            mut plan,
560        } = downstream;
561
562        // This chain's stages, in front of the plan that arrived. What the
563        // branch ends in is untouched: it was assembled by its own `to`.
564        let mut nodes: Vec<_> = self.planned.iter().map(|node| node.info.clone()).collect();
565        let mut edges = Vec::with_capacity(self.planned.len() + plan.edges.len());
566        for (index, node) in self.planned.iter().enumerate() {
567            // The last stage feeds the branch's root; every other one feeds
568            // the stage after it.
569            let consumer = self
570                .planned
571                .get(index + 1)
572                .map_or(plan.root, |next| next.info.id);
573            edges.push(PlannedEdge {
574                from: PortRef {
575                    element: node.info.id,
576                    port: node.output_port.clone(),
577                },
578                to: PortRef {
579                    element: consumer,
580                    port: "sink".into(),
581                },
582            });
583            plan.contracts.insert(
584                node.info.id,
585                PortContracts {
586                    input: node.input,
587                    output: node.output,
588                },
589            );
590        }
591        // A chain with no stages of its own is the branch itself, root
592        // included — there is nothing in front of it to become the new one.
593        let root_id = nodes.first().map_or(plan.root, |node| node.id);
594        edges.append(&mut plan.edges);
595        nodes.append(&mut plan.nodes);
596        let plan = BranchPlan {
597            nodes,
598            edges,
599            contracts: plan.contracts,
600            root: root_id,
601        };
602        // The same walk `to` runs, over the joined plan: a stage added in
603        // front can be what makes a link below the branch's root impossible.
604        plan.resolve(None)?;
605
606        let root = self
607            .elements
608            .into_iter()
609            .rev()
610            .try_fold(sink, |downstream, stage| {
611                stage.wrap(downstream, &self.context)
612            })?;
613        Ok(DetachedBranch { root, plan })
614    }
615
616    /// Alias of [`Self::to`] retained for callers that prefer builder-style
617    /// terminology when supplying the terminal sink.
618    pub fn build(self, terminal: Box<dyn Sink>) -> Result<DetachedBranch> {
619        self.to(terminal)
620    }
621}
622
623impl Context {
624    /// Starts a detached branch plan scoped to this source and pipeline.
625    ///
626    /// Building the branch allocates its runtime elements but does not publish
627    /// them in the graph until [`Self::attach`] succeeds.
628    pub fn branch(self: &Arc<Self>) -> ChainBuilder {
629        ChainBuilder::new(self.clone())
630    }
631
632    /// Atomically attaches a completed branch to one source pad.
633    ///
634    /// Returns the stable branch identity used by later dynamic graph
635    /// operations. An invalid index or already-linked pad returns a
636    /// [`GraphError`] without changing either the runtime connection or graph.
637    /// If a lifecycle or seek operation is in progress, this returns
638    /// [`GraphError::TimelineOperationInProgress`] immediately; build a fresh
639    /// detached branch and retry after that operation finishes.
640    pub fn attach<S: Source>(
641        &self,
642        source: &mut S,
643        pad_index: usize,
644        branch: DetachedBranch,
645    ) -> Result<BranchId> {
646        let pads = source.src_pads();
647        let pad_count = pads.len();
648        let pad = pads.get_mut(pad_index).ok_or(GraphError::PadOutOfRange {
649            index: pad_index,
650            pad_count,
651        })?;
652        self.attach_pad(pad, branch)
653    }
654
655    pub(crate) fn attach_pad(&self, pad: &mut SrcPad, branch: DetachedBranch) -> Result<BranchId> {
656        let _operation = match self.operation.try_lock() {
657            Ok(guard) => guard,
658            Err(std::sync::TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
659            Err(std::sync::TryLockError::WouldBlock) => {
660                return Err(GraphError::TimelineOperationInProgress.into());
661            }
662        };
663        if pad.is_linked() {
664            return Err(GraphError::PadAlreadyLinked(pad.name().to_owned()).into());
665        }
666        let from_port: Arc<str> = pad.name().into();
667        // Where a branch built in isolation finally meets the pad feeding
668        // it. Re-walking the whole branch rather than comparing one
669        // summarized input contract is what makes a leading passthrough
670        // stage work: it carries this pad's contract through to whichever
671        // element downstream actually constrains it. The check runs inside
672        // `attach_with`'s own transaction, so a rejected branch leaves the
673        // pad unlinked and the graph untouched.
674        let incoming = Incoming::Known(incoming_from(pad));
675        let DetachedBranch { root, plan } = branch;
676        Ok(self
677            .graph
678            .attach_with(self.source_id, from_port, incoming, plan, |_| {
679                pad.link(root);
680                Ok(())
681            })?)
682    }
683}
684
685/// What a pad is known to be putting onto the wire, for
686/// [`BranchPlan::resolve`].
687///
688/// A [`OutputContract::Passthrough`] pad — a [`crate::elements::Tee`]'s —
689/// carries whatever reached the element that owns it, which this cannot
690/// see from the pad alone; those are resolved from the live graph instead
691/// (see [`crate::elements::TeeHandle::attach`]).
692pub(crate) fn incoming_from(pad: &SrcPad) -> Option<ResolvedFlow> {
693    match pad.contract() {
694        OutputContract::Fixed(contract) => Some(ResolvedFlow {
695            producer: pad.name().into(),
696            contract,
697        }),
698        OutputContract::Passthrough | OutputContract::Unknown => None,
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use std::sync::{
705        Arc,
706        atomic::{AtomicUsize, Ordering},
707    };
708
709    use super::*;
710    use crate::{control::PrerollContext, element::element_pp_log};
711
712    struct CountingTerminal {
713        count: Arc<AtomicUsize>,
714        pp_log: PpLog,
715    }
716
717    impl Element for CountingTerminal {
718        fn name(&self) -> Arc<str> {
719            "terminal".into()
720        }
721
722        fn element_type(&self) -> ElementType {
723            ElementType::Other
724        }
725
726        fn pp_log(&self) -> &PpLog {
727            &self.pp_log
728        }
729
730        fn pp_log_mut(&mut self) -> &mut PpLog {
731            &mut self.pp_log
732        }
733    }
734
735    impl Sink for CountingTerminal {
736        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
737            self.count.fetch_add(1, Ordering::SeqCst);
738            Ok(())
739        }
740
741        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
742            Ok(())
743        }
744    }
745
746    fn terminal(id: ElementId, count: Arc<AtomicUsize>) -> TerminalTracer {
747        let (bus, _rx) = Bus::new();
748        TerminalTracer {
749            bus: bus.for_element(id),
750            id,
751            inner: Box::new(CountingTerminal {
752                count,
753                pp_log: element_pp_log(ElementType::Other, "terminal", None),
754            }),
755            paused: false,
756            preroll: None,
757        }
758    }
759
760    /// Each terminal closes on its *own* sample. Holding it open until every
761    /// branch is done would let whichever reached the target first keep
762    /// consuming for as long as the slowest takes, so the two streams would
763    /// sit at different positions once preroll completed.
764    #[test]
765    fn terminal_readiness_closes_on_its_own_sample_not_the_whole_preroll() {
766        let first = ElementId::for_test(1);
767        let second = ElementId::for_test(2);
768        let context = Arc::new(PrerollContext::new([first, second]));
769        let mut first_terminal = terminal(first, Arc::new(AtomicUsize::new(0)));
770        let mut second_terminal = terminal(second, Arc::new(AtomicUsize::new(0)));
771
772        first_terminal
773            .control(ControlMsg::Pause)
774            .expect("pause first terminal");
775        assert!(!first_terminal.ready_consume());
776        first_terminal
777            .control(ControlMsg::Preroll(Arc::clone(&context)))
778            .expect("preroll first terminal");
779        second_terminal
780            .control(ControlMsg::Preroll(Arc::clone(&context)))
781            .expect("preroll second terminal");
782
783        first_terminal
784            .consume(MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty())))
785            .expect("consume first terminal sample");
786        assert!(
787            !first_terminal.ready_consume(),
788            "the first terminal stops as soon as it has its own sample"
789        );
790        assert!(
791            second_terminal.ready_consume(),
792            "the second is still owed one"
793        );
794        assert!(!context.is_complete());
795
796        second_terminal
797            .consume(MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty())))
798            .expect("consume second terminal sample");
799        assert!(context.is_complete());
800        assert!(!first_terminal.ready_consume());
801        assert!(!second_terminal.ready_consume());
802
803        first_terminal
804            .control(ControlMsg::Resume)
805            .expect("resume first terminal");
806        assert!(first_terminal.ready_consume());
807    }
808}