Skip to main content

g2g_core/runtime/
autoplug.rs

1//! Auto-plug: a runtime element registry plus a decode-chain search over the
2//! static pad-template metadata (DESIGN.md ยง4.13.7, DESIGN_TODO "Auto-plug /
3//! element registry / `decodebin`-equivalent"). M83.
4//!
5//! GStreamer's `decodebin` takes the caps coming off a source and walks the
6//! registry for a chain of element factories whose pad templates compose from
7//! that input down to raw, then instantiates the chain as a bin. We have the
8//! type-level metadata already ([`PadTemplates`], [`PadTemplate`]) and a solver
9//! that answers "can A's source feed B's sink?" ([`pad_link`]); what was
10//! missing was (a) a runtime enumeration of element types and (b) the search
11//! that composes their templates into an ordered chain.
12//!
13//! Two layers, split by what they need:
14//! - **Search** (`runtime`, no_std + alloc). [`ElementDesc`] is a name + its
15//!   pad templates; [`find_chain`] runs a breadth-first search over caps states,
16//!   each edge an element whose sink accepts the current caps, until an
17//!   element's source produces caps satisfying the target. Shortest chain wins.
18//!   This is the intellectual core and is testable without constructing a
19//!   single element.
20//! - **Registry** (`std`). [`Registry`] pairs each [`ElementDesc`] with a
21//!   parameterless factory producing a boxed [`DynAsyncElement`], so
22//!   [`Registry::autoplug`] returns the instantiated chain ready to splice onto
23//!   [`run_graph`](crate::runtime::run_graph) as a sub-graph of transforms.
24//!
25//! The search picks element *types*; it does not fixate geometry or framerate.
26//! A decoder's source template is "raw video at any geometry", so the search
27//! state stays open and the concrete values are chosen later at instance
28//! negotiation when the chain is run. The target is therefore a shape predicate
29//! (see [`is_raw_video`]), not a fixed caps.
30
31use alloc::vec::Vec;
32
33use crate::caps::{AudioFormat, Caps, CapsSet};
34use crate::memory::MemoryDomainKind;
35use crate::pad_template::{pad_link, PadCaps, PadDirection, PadTemplate};
36use crate::runtime::solver::NegotiationFailure;
37
38/// Whether an element's path is hardware-accelerated. This is independent of
39/// where the output frames land ([`CapabilityDescriptor::output_memory`]): an
40/// ffmpeg VA-API decoder is hardware yet downloads to `System`, while a CPU
41/// decoder is software and `System`. Auto-plug can prefer / avoid hardware per
42/// request (throughput vs power) separately from the memory domain.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Acceleration {
45    /// CPU / pure-software path (the default).
46    #[default]
47    Software,
48    /// Fixed-function / GPU / platform hardware decode or encode.
49    Hardware,
50}
51
52/// What an element offers the auto-plug search beyond its pad templates: the
53/// signals used to choose among several elements that satisfy the same caps.
54///
55/// This generalizes a bare output-memory tag, the principled alternative to a
56/// flat global rank. A single integer cannot express that the best element is
57/// context-dependent (a hardware decoder that keeps frames on the GPU beats a
58/// faster one that forces a PCIe download when the consumer is GPU-resident), so
59/// [`score`](Self::score) ranks a candidate against a [`SelectionContext`]; the
60/// numeric [`rank`](Self::rank) is only the deterministic tiebreaker among
61/// otherwise-equal candidates (the explicit-override knob GStreamer's rank gets
62/// right). All-default descriptors score equally, so registration order decides,
63/// leaving a plain pipeline's selection unchanged.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct CapabilityDescriptor {
66    /// The memory feature the source pad emits (a `decodebin` caps feature,
67    /// GStreamer's `memory:CUDAMemory` analog). Caps geometry / format is in the
68    /// pad templates; the *memory domain* is not, so it rides here. Matching the
69    /// consumer's domain avoids a copy / download (e.g. pick `NvDec` -> `Cuda`).
70    pub output_memory: MemoryDomainKind,
71    /// Hardware vs software path.
72    pub acceleration: Acceleration,
73    /// Deterministic tiebreaker among candidates of equal score (higher wins);
74    /// default 0. Use it to order otherwise-equivalent backends (e.g. two ffmpeg
75    /// codecs), or as an operator override, not as the primary selector.
76    pub rank: i16,
77}
78
79impl Default for CapabilityDescriptor {
80    fn default() -> Self {
81        Self {
82            output_memory: MemoryDomainKind::System,
83            acceleration: Acceleration::Software,
84            rank: 0,
85        }
86    }
87}
88
89impl CapabilityDescriptor {
90    /// Score this candidate against what the search wants; higher is tried first.
91    /// Memory-domain match dominates (it removes a download, the structural win),
92    /// then a hardware preference, then the explicit [`rank`](Self::rank).
93    pub fn score(&self, ctx: &SelectionContext) -> i32 {
94        let mut s = 0i32;
95        if self.output_memory == ctx.preferred_memory {
96            s += 1000;
97        }
98        if ctx.prefer_hardware && self.acceleration == Acceleration::Hardware {
99            s += 100;
100        }
101        s + self.rank as i32
102    }
103}
104
105/// What the auto-plug search optimizes for among elements that satisfy the same
106/// caps. The default (`System` memory, no hardware preference) reproduces a plain
107/// pipeline's selection, so existing behavior is unchanged.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub struct SelectionContext {
110    /// The memory domain the consumer wants the frames in (avoids a download when
111    /// it matches a producer's [`CapabilityDescriptor::output_memory`]).
112    pub preferred_memory: MemoryDomainKind,
113    /// Prefer a hardware path when one is available.
114    pub prefer_hardware: bool,
115}
116
117/// An element type's autoplug-relevant metadata: a display name and its static
118/// pad templates (typically `<E as PadTemplates>::pad_templates()`). The search
119/// reads only the first sink and first source template; multi-pad elements
120/// (tees, muxers) are not auto-plug candidates and are simply never matched.
121#[derive(Debug, Clone)]
122pub struct ElementDesc {
123    /// Human-readable type name, used to report the chosen chain.
124    pub name: &'static str,
125    /// The element type's pad templates, in declaration order.
126    pub templates: Vec<PadTemplate>,
127    /// Auto-plug selection signals (output memory, hardware, rank): how the
128    /// search picks this among elements satisfying the same caps. See
129    /// [`CapabilityDescriptor`].
130    pub capabilities: CapabilityDescriptor,
131}
132
133impl ElementDesc {
134    /// Build a descriptor from a name and its pad templates. The capabilities
135    /// default (software, `System` memory, rank 0); tag a GPU / hardware producer
136    /// with the builders below.
137    pub fn new(name: &'static str, templates: Vec<PadTemplate>) -> Self {
138        Self {
139            name,
140            templates,
141            capabilities: CapabilityDescriptor::default(),
142        }
143    }
144
145    /// Tag the memory feature this element's source pad produces. Builder form.
146    pub fn with_output_memory(mut self, kind: MemoryDomainKind) -> Self {
147        self.capabilities.output_memory = kind;
148        self
149    }
150
151    /// Tag this element's path as hardware-accelerated. Builder form.
152    pub fn with_acceleration(mut self, acceleration: Acceleration) -> Self {
153        self.capabilities.acceleration = acceleration;
154        self
155    }
156
157    /// Set the deterministic tiebreaker rank (higher wins among equal scores).
158    pub fn with_rank(mut self, rank: i16) -> Self {
159        self.capabilities.rank = rank;
160        self
161    }
162
163    /// The memory feature the element's source pad emits.
164    pub fn output_memory(&self) -> MemoryDomainKind {
165        self.capabilities.output_memory
166    }
167
168    /// First sink (input) pad template, if any.
169    fn sink(&self) -> Option<&PadTemplate> {
170        self.templates
171            .iter()
172            .find(|t| t.direction == PadDirection::Sink)
173    }
174
175    /// First source (output) pad template, if any.
176    fn source(&self) -> Option<&PadTemplate> {
177        self.templates
178            .iter()
179            .find(|t| t.direction == PadDirection::Source)
180    }
181
182    /// If this element accepts caps shaped like `input` on its sink pad, the
183    /// caps set its source pad can then produce; `None` if it has no sink or
184    /// source pad, or its sink rejects `input`.
185    ///
186    /// Acceptance reuses the negotiation solver: `input` is wrapped as a
187    /// producer and linked against the sink template. An `Unfixable` link (both
188    /// sides still open, e.g. geometry `Any` feeding `Any`) counts as accepted,
189    /// since the search resolves shapes, not concrete values, exactly as
190    /// [`types_can_link`](crate::pad_template::types_can_link) does.
191    fn step(&self, input: &Caps) -> Option<CapsSet> {
192        let sink = self.sink()?;
193        let source = self.source()?;
194        let input_as_src = PadTemplate::source(CapsSet::one(input.clone()));
195        match pad_link(&input_as_src, sink) {
196            Ok(_) | Err(NegotiationFailure::Unfixable { .. }) => match &source.caps {
197                PadCaps::Fixed(set) => Some(set.clone()),
198                // A wildcard source pad produces nothing concrete to advance on.
199                PadCaps::Any => None,
200            },
201            _ => None,
202        }
203    }
204}
205
206/// Shape predicate: the caps are raw (decoded) video. The canonical
207/// `decodebin` target, "walk from this input down to raw video."
208pub fn is_raw_video(caps: &Caps) -> bool {
209    matches!(caps, Caps::RawVideo { .. })
210}
211
212/// Shape predicate: the caps are raw (decoded) PCM audio. The audio half of the
213/// `decodebin` target, "walk down to raw audio." [`Caps::Audio`] is overloaded:
214/// it also carries compressed AAC / Opus (the demuxer / parser output), so this
215/// matches only the PCM formats, not a compressed stream still labelled `Audio`.
216pub fn is_raw_audio(caps: &Caps) -> bool {
217    matches!(
218        caps,
219        Caps::Audio {
220            format: AudioFormat::PcmS16Le | AudioFormat::PcmF32Le,
221            ..
222        }
223    )
224}
225
226/// One element on an auto-plugged chain: which registered [`ElementDesc`] it is
227/// (`index` into the searched slice) and the output caps the search chose for it
228/// (the source-pad alternative it was matched to produce). The caps pin the
229/// media type and format the element must emit, which a format-flexible element
230/// (a converter, a multi-format decoder) needs to be constructed; geometry and
231/// framerate may still be open and fixate later at instance negotiation.
232#[derive(Debug, Clone, PartialEq)]
233pub struct ChainLink {
234    /// Index into the `descs` slice passed to [`find_chain`].
235    pub index: usize,
236    /// The caps this element was chosen to produce on its source pad.
237    pub output: Caps,
238}
239
240/// Find the shortest chain of registered element types that converts `input`
241/// caps into caps satisfying `target`, returning the chain in order (upstream
242/// first): for each hop, the descriptor index and the output caps the search
243/// picked for it.
244///
245/// Returns `Some(vec![])` if `input` already satisfies `target` (no elements
246/// needed), or `None` if no chain exists within `max_depth` hops. The search is
247/// breadth-first over caps states, so the first chain found is the shortest. An
248/// element is never used twice on the same path, which terminates same-shape
249/// loops (e.g. a parser whose sink and source are both H.264).
250pub fn find_chain(
251    descs: &[ElementDesc],
252    input: &Caps,
253    target: &dyn Fn(&Caps) -> bool,
254    max_depth: usize,
255) -> Option<Vec<ChainLink>> {
256    find_chain_preferring(descs, input, target, max_depth, MemoryDomainKind::System)
257}
258
259/// Like [`find_chain`], but biases ties toward a chain whose terminal element
260/// emits the `preferred` memory feature. Thin wrapper over [`find_chain_with`]
261/// with a memory-only [`SelectionContext`].
262pub fn find_chain_preferring(
263    descs: &[ElementDesc],
264    input: &Caps,
265    target: &dyn Fn(&Caps) -> bool,
266    max_depth: usize,
267    preferred: MemoryDomainKind,
268) -> Option<Vec<ChainLink>> {
269    find_chain_with(
270        descs,
271        input,
272        target,
273        max_depth,
274        SelectionContext {
275            preferred_memory: preferred,
276            prefer_hardware: false,
277        },
278    )
279}
280
281/// Like [`find_chain`], but scores candidates against `ctx` ([`SelectionContext`])
282/// to choose among elements that satisfy the same caps. The search is still
283/// breadth-first, so a shorter chain always wins; among equal-length chains, the
284/// higher-scoring candidate ([`CapabilityDescriptor::score`]) is tried first, so a
285/// GPU consumer can request `Cuda` and get `NvDec` ahead of a CPU decoder.
286///
287/// The context only reorders which candidate is *tried first* at each hop; if no
288/// chain matching the preference exists, the search still finds any valid one. A
289/// default `ctx` scores every candidate equally, so the visit order is
290/// registration order and a plain pipeline's selection is unchanged.
291pub fn find_chain_with(
292    descs: &[ElementDesc],
293    input: &Caps,
294    target: &dyn Fn(&Caps) -> bool,
295    max_depth: usize,
296    ctx: SelectionContext,
297) -> Option<Vec<ChainLink>> {
298    if target(input) {
299        return Some(Vec::new());
300    }
301    // Visit order: highest score first, ties broken by registration order (the
302    // sort is stable). A default context scores all candidates 0, so this is
303    // registration order (no behavior change).
304    let mut order: Vec<usize> = (0..descs.len()).collect();
305    order.sort_by_key(|&i| core::cmp::Reverse(descs[i].capabilities.score(&ctx)));
306    // BFS frontier: each entry is a reached caps state and the element path
307    // that produced it. Depth is bounded by max_depth so an unsatisfiable
308    // target terminates even with cycle-free same-shape elements.
309    let mut frontier: Vec<(Caps, Vec<ChainLink>)> = Vec::from([(input.clone(), Vec::new())]);
310    for _ in 0..max_depth {
311        let mut next: Vec<(Caps, Vec<ChainLink>)> = Vec::new();
312        for (caps, path) in &frontier {
313            for &i in &order {
314                if path.iter().any(|link| link.index == i) {
315                    continue;
316                }
317                let desc = &descs[i];
318                let Some(out_set) = desc.step(caps) else {
319                    continue;
320                };
321                for out in out_set.alternatives() {
322                    let mut new_path = path.clone();
323                    new_path.push(ChainLink {
324                        index: i,
325                        output: out.clone(),
326                    });
327                    if target(out) {
328                        return Some(new_path);
329                    }
330                    next.push((out.clone(), new_path));
331                }
332            }
333        }
334        if next.is_empty() {
335            return None;
336        }
337        frontier = next;
338    }
339    None
340}
341
342/// The media kind a demux output-pad / muxer input-pad reference names (M476,
343/// input side M481): `video_0` -> [`Video`](PadKind::Video), `audio_1` ->
344/// [`Audio`](PadKind::Audio), a bare `d.` / `m.` or `src_2` / `sink_2` ->
345/// [`Any`](PadKind::Any). Defined outside the std-gated `factory` module because
346/// the `no_std` fan-in trait ([`MultiInputElement::input_pad_index`]) uses it.
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum PadKind {
349    Video,
350    Audio,
351    Text,
352    Any,
353}
354
355/// A parsed pad reference from a `gst-launch` line (M476 output / M481 input),
356/// e.g. `d.video_0` -> `{ Video, 0 }`, `m.audio_1` -> `{ Audio, 1 }`, a bare `d.`
357/// -> `{ Any, n }`. Handed to a [`DemuxSelectHook`] (output) or
358/// [`MultiInputElement::input_pad_index`] (input) to map each request to a stream
359/// or input index.
360///
361/// [`MultiInputElement::input_pad_index`]: crate::MultiInputElement::input_pad_index
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct PadRequest {
364    pub kind: PadKind,
365    pub index: usize,
366}
367
368/// Names the encoders that produce a coded target caps, most preferred first
369/// (the `encodebin` expansion takes the first one this build registered).
370pub type EncoderProvider = fn(&Caps) -> Option<&'static [EncoderChoice]>;
371
372/// Names the muxers that write a container caps, most preferred first.
373pub type MuxerProvider = fn(&Caps) -> Option<&'static [&'static str]>;
374
375/// One encoder an encoding profile's stream can be produced by: the launch
376/// element and the properties that pin it to that codec (a multi-codec encoder
377/// like `vpxenc` or `nvenc` selects with `codec=`). The `encodebin` expansion
378/// takes the first choice whose element this build registered.
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct EncoderChoice {
381    pub element: &'static str,
382    pub props: &'static [(&'static str, &'static str)],
383}
384
385impl EncoderChoice {
386    /// An encoder that needs no property to produce this codec.
387    pub const fn plain(element: &'static str) -> Self {
388        Self {
389            element,
390            props: &[],
391        }
392    }
393
394    /// An encoder whose codec is selected by properties.
395    pub const fn with_props(
396        element: &'static str,
397        props: &'static [(&'static str, &'static str)],
398    ) -> Self {
399        Self { element, props }
400    }
401}
402
403#[cfg(feature = "std")]
404mod factory {
405    use super::*;
406    use alloc::boxed::Box;
407
408    use alloc::format;
409    use alloc::string::{String, ToString};
410
411    use crate::element::{AsyncElement, DynAsyncElement};
412    use crate::fanout::MultiOutputElement;
413    use crate::graph::{Graph, GraphError, NodeId, PadId};
414    use crate::memory::DomainSet;
415    use crate::pad_template::{PadCaps, PadDirection, PadTemplate, PadTemplates};
416    use crate::property::{format_specs, PropError, PropValue};
417    use crate::runtime::launch::ParseError;
418    use crate::runtime::{
419        DynMultiInputElement, DynMultiOutputElement, DynSourceLoop, GraphNode, GraphNodeRef,
420    };
421
422    /// Structured, owned introspection of one registered element: the same facts
423    /// [`Registry::inspect`] renders as a `gst-inspect` text dump, exposed as data
424    /// so external tooling (the searchable element reference on the docs site,
425    /// generated by `g2g-docgen`) reads from one source of truth. Built by
426    /// [`Registry::describe`] / [`Registry::describe_all`].
427    #[derive(Debug, Clone)]
428    pub struct ElementDoc {
429        /// Registry / `gst-launch` name.
430        pub name: String,
431        /// Human-readable long name (may be empty).
432        pub long_name: String,
433        /// Classification (`klass`), e.g. `"Codec/Encoder/Audio"`.
434        pub klass: String,
435        /// One-paragraph description of what the element does.
436        pub description: String,
437        /// Author / origin.
438        pub author: String,
439        /// `"source"`, `"element"`, or `"muxer (fan-in)"`.
440        pub role: String,
441        /// Host- or device-only runtime path: compiled, not promised in CI.
442        /// See `STABILITY.md` (Tier 3).
443        pub experimental: bool,
444        /// Output caps (sources / muxers), the debug spelling; `None` for a
445        /// transform / sink, which advertises pad templates instead.
446        pub caps: Option<String>,
447        /// Pad templates as `"DIR: caps"` lines (transforms / sinks).
448        pub pads: Vec<String>,
449        /// Settable properties, in declaration order.
450        pub properties: Vec<PropertyDoc>,
451    }
452
453    /// One settable property, owned (the [`ElementDoc`] counterpart of a
454    /// [`PropertySpec`](crate::PropertySpec)).
455    #[derive(Debug, Clone)]
456    pub struct PropertyDoc {
457        /// Property name (`key=value` in a launch line).
458        pub name: String,
459        /// One-line description.
460        pub blurb: String,
461        /// Type label as `gst-inspect` names it (`"String"`, `"Unsigned Integer"`).
462        pub type_label: String,
463        /// Default value as text, if any.
464        pub default: Option<String>,
465        /// Accepted `(min, max)` range as text, for a numeric property.
466        pub range: Option<(String, String)>,
467        /// Named choices of an enum-like string property.
468        pub enum_values: Option<String>,
469        pub readable: bool,
470        pub writable: bool,
471    }
472
473    /// Owned [`PropertyDoc`]s from a static spec table (shared by `describe`'s
474    /// three element-kind branches).
475    fn property_docs(specs: &[crate::property::PropertySpec]) -> Vec<PropertyDoc> {
476        specs
477            .iter()
478            .map(|s| PropertyDoc {
479                name: s.name.to_string(),
480                blurb: s.blurb.to_string(),
481                type_label: s.kind.label().to_string(),
482                default: s.default.map(|d| d.to_string()),
483                range: s.range.map(|(a, b)| (a.to_string(), b.to_string())),
484                enum_values: s.enum_values.map(|v| v.to_string()),
485                readable: s.flags.readable,
486                writable: s.flags.writable,
487            })
488            .collect()
489    }
490
491    /// Pad templates as `"DIR: caps"` lines, matching `format_templates`.
492    fn pad_docs(templates: &[PadTemplate]) -> Vec<String> {
493        templates
494            .iter()
495            .map(|t| {
496                let dir = match t.direction {
497                    PadDirection::Sink => "SINK",
498                    PadDirection::Source => "SRC",
499                };
500                match &t.caps {
501                    PadCaps::Fixed(set) => format!("{dir}: {:?}", set.alternatives()),
502                    PadCaps::Any => format!("{dir}: ANY"),
503                }
504            })
505            .collect()
506    }
507
508    /// A registered element type: its autoplug metadata plus a constructor
509    /// producing a boxed transform/sink for the graph runner. The constructor
510    /// receives the output caps the search chose for this hop (see
511    /// [`ChainLink::output`]), so a format-flexible element configures itself to
512    /// produce the right format. It is a plain `fn` pointer, the common case
513    /// being a non-capturing closure `|out| Box::new(MyTransform::new(out))`
514    /// coerced at the call site; an element with a fixed output ignores the arg
515    /// (`|_| Box::new(MyDecoder::new())`).
516    pub struct ElementFactory {
517        desc: ElementDesc,
518        build: fn(&Caps) -> Box<dyn DynAsyncElement>,
519    }
520
521    impl ElementFactory {
522        /// Register an element type by name, pad templates, and constructor.
523        pub fn new(
524            name: &'static str,
525            templates: Vec<PadTemplate>,
526            build: fn(&Caps) -> Box<dyn DynAsyncElement>,
527        ) -> Self {
528            Self {
529                desc: ElementDesc::new(name, templates),
530                build,
531            }
532        }
533
534        /// Build from a [`PadTemplates`] type, pulling its templates from the
535        /// trait so the registration site names only the type and constructor.
536        pub fn of<E: PadTemplates>(
537            name: &'static str,
538            build: fn(&Caps) -> Box<dyn DynAsyncElement>,
539        ) -> Self {
540            Self::new(name, E::pad_templates(), build)
541        }
542
543        /// Tag the memory feature this factory's element produces on its source
544        /// pad (see [`ElementDesc::output_memory`]); used by the domain-aware
545        /// auto-plug search to prefer e.g. a `Cuda` producer for a GPU consumer.
546        /// Builder form, defaulting to `System`.
547        pub fn produces(mut self, kind: crate::memory::MemoryDomainKind) -> Self {
548            self.desc = self.desc.with_output_memory(kind);
549            self
550        }
551
552        /// Tag this factory's element as a hardware-accelerated path, so a
553        /// hardware-preferring auto-plug search ([`SelectionContext::prefer_hardware`])
554        /// favors it. Builder form.
555        pub fn hardware(mut self) -> Self {
556            self.desc = self.desc.with_acceleration(Acceleration::Hardware);
557            self
558        }
559
560        /// Set the deterministic tiebreaker rank (higher wins among candidates of
561        /// equal score); the explicit-override knob. Builder form, default 0.
562        pub fn rank(mut self, rank: i16) -> Self {
563            self.desc = self.desc.with_rank(rank);
564            self
565        }
566
567        /// Instantiate a fresh boxed element configured to produce `output`.
568        pub fn build(&self, output: &Caps) -> Box<dyn DynAsyncElement> {
569            (self.build)(output)
570        }
571
572        /// This factory's autoplug descriptor.
573        pub fn desc(&self) -> &ElementDesc {
574            &self.desc
575        }
576    }
577
578    /// A named element factory for the `gst-launch` text parser and the
579    /// `gst-inspect` dump (M105): a *parameterless* constructor plus the element's
580    /// pad templates. Unlike [`ElementFactory`] (the autoplug factory, built from
581    /// the chosen output caps), this default-constructs the element so the parser
582    /// can then apply `key=value` properties to it, the
583    /// `gst_element_factory_make` + `g_object_set` model.
584    pub struct LaunchFactory {
585        name: &'static str,
586        templates: Vec<PadTemplate>,
587        build: fn() -> Box<dyn DynAsyncElement>,
588        usable: Option<fn() -> bool>,
589        experimental: bool,
590    }
591
592    impl LaunchFactory {
593        /// Register a transform / sink by name, pad templates, and a
594        /// parameterless constructor (`|| Box::new(MyElement::new())`).
595        pub fn new(
596            name: &'static str,
597            templates: Vec<PadTemplate>,
598            build: fn() -> Box<dyn DynAsyncElement>,
599        ) -> Self {
600            Self {
601                name,
602                templates,
603                build,
604                usable: None,
605                experimental: false,
606            }
607        }
608
609        /// Build from a [`PadTemplates`] type, pulling its templates from the
610        /// trait so the registration site names only the type and constructor.
611        pub fn of<E: PadTemplates>(
612            name: &'static str,
613            build: fn() -> Box<dyn DynAsyncElement>,
614        ) -> Self {
615            Self::new(name, E::pad_templates(), build)
616        }
617
618        /// Declare a check for whether this element can run on this machine
619        /// right now, beyond having been compiled in: a display sink asking
620        /// whether there is a display to present on, say.
621        ///
622        /// Only [`register_alias`](Registry::register_alias) consults it, so an
623        /// `autovideosink` falls through a sink with nothing to draw on while a
624        /// pipeline naming that sink outright still fails and says why.
625        pub fn with_usable(mut self, usable: fn() -> bool) -> Self {
626            self.usable = Some(usable);
627            self
628        }
629
630        /// Mark this element experimental: its runtime path is host- or
631        /// device-validated, not a CI promise. `g2g-inspect` prints it.
632        pub fn with_experimental(mut self) -> Self {
633            self.experimental = true;
634            self
635        }
636
637        /// Whether this factory declared [`with_experimental`].
638        pub fn experimental(&self) -> bool {
639            self.experimental
640        }
641
642        /// This factory's element name.
643        pub fn name(&self) -> &'static str {
644            self.name
645        }
646
647        /// Whether this element can run here, per the check it declared.
648        /// `true` when it declared none.
649        pub fn usable(&self) -> bool {
650            self.usable.is_none_or(|check| check())
651        }
652    }
653
654    impl core::fmt::Debug for LaunchFactory {
655        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
656            f.debug_struct("LaunchFactory")
657                .field("name", &self.name)
658                .finish_non_exhaustive()
659        }
660    }
661
662    /// A named fan-in muxer factory for the `gst-launch` parser (M122): an
663    /// N-to-1 element built per use with the input count the parser derives from
664    /// link degree. Unlike [`LaunchFactory`] (a single-in / single-out transform
665    /// or sink), the constructor takes the input count, because a
666    /// [`MultiInputElement`](crate::MultiInputElement)'s `input_count` must match
667    /// the muxer node's input-pad count.
668    pub struct MuxerFactory {
669        name: &'static str,
670        build: fn(usize) -> Box<dyn DynMultiInputElement>,
671    }
672
673    impl MuxerFactory {
674        /// Register a fan-in muxer by name and an input-count constructor
675        /// (`|n| Box::new(MyMux::new(n, ...))`).
676        pub fn new(name: &'static str, build: fn(usize) -> Box<dyn DynMultiInputElement>) -> Self {
677            Self { name, build }
678        }
679
680        /// This factory's element name.
681        pub fn name(&self) -> &'static str {
682            self.name
683        }
684    }
685
686    impl core::fmt::Debug for MuxerFactory {
687        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
688            f.debug_struct("MuxerFactory")
689                .field("name", &self.name)
690                .finish_non_exhaustive()
691        }
692    }
693
694    /// A named fan-out demuxer factory for the `gst-launch` parser (M210): the
695    /// transpose of [`MuxerFactory`]. A 1-to-N element built per use with the
696    /// output count the parser derives from link degree (the `d.` references),
697    /// because a [`MultiOutputElement`](crate::fanout::MultiOutputElement)'s
698    /// output-pad count must match the demux node's fan-out.
699    pub struct DemuxFactory {
700        name: &'static str,
701        build: fn(usize) -> Box<dyn DynMultiOutputElement>,
702    }
703
704    /// Factory for a terminal fan-out *source* (M727): a 0-in / N-out
705    /// [`MultiOutputSource`](crate::fanout::MultiOutputSource) generating every
706    /// output itself (a WebRTC session receiving its tracks). The build takes
707    /// the linked output count so the parser can validate it against the
708    /// element's intrinsic port count.
709    pub struct FanoutSrcFactory {
710        name: &'static str,
711        build: fn(usize) -> Box<dyn crate::fanout::DynMultiOutputSource>,
712    }
713
714    impl FanoutSrcFactory {
715        pub fn new(
716            name: &'static str,
717            build: fn(usize) -> Box<dyn crate::fanout::DynMultiOutputSource>,
718        ) -> Self {
719            Self { name, build }
720        }
721
722        /// This factory's element name.
723        pub fn name(&self) -> &'static str {
724            self.name
725        }
726    }
727
728    impl core::fmt::Debug for FanoutSrcFactory {
729        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
730            f.debug_struct("FanoutSrcFactory")
731                .field("name", &self.name)
732                .finish_non_exhaustive()
733        }
734    }
735
736    impl DemuxFactory {
737        /// Register a fan-out demuxer by name and an output-count constructor
738        /// (`|n| Box::new(MyDemux::new(n, ...))`).
739        pub fn new(name: &'static str, build: fn(usize) -> Box<dyn DynMultiOutputElement>) -> Self {
740            Self { name, build }
741        }
742
743        /// This factory's element name.
744        pub fn name(&self) -> &'static str {
745            self.name
746        }
747    }
748
749    impl core::fmt::Debug for DemuxFactory {
750        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
751            f.debug_struct("DemuxFactory")
752                .field("name", &self.name)
753                .finish_non_exhaustive()
754        }
755    }
756
757    /// Format an element's pad templates the way `gst-inspect` lists them (one
758    /// line per pad: direction + the caps it accepts/produces).
759    fn format_templates(templates: &[PadTemplate]) -> String {
760        use core::fmt::Write;
761        let mut out = String::new();
762        for t in templates {
763            let dir = match t.direction {
764                PadDirection::Sink => "SINK",
765                PadDirection::Source => "SRC",
766            };
767            match &t.caps {
768                PadCaps::Fixed(set) => {
769                    let _ = writeln!(out, "  {dir}: {:?}", set.alternatives());
770                }
771                PadCaps::Any => {
772                    let _ = writeln!(out, "  {dir}: ANY");
773                }
774            }
775        }
776        out
777    }
778
779    /// Why [`Registry::decodebin`] could not splice a chain.
780    #[derive(Debug)]
781    pub enum DecodebinError {
782        /// No chain of registered elements converts the input caps to the target
783        /// within the depth bound.
784        NoChain,
785        /// A graph link failed (e.g. a pad was out of range or already linked).
786        Graph(GraphError),
787    }
788
789    impl From<GraphError> for DecodebinError {
790        fn from(e: GraphError) -> Self {
791            DecodebinError::Graph(e)
792        }
793    }
794
795    /// The representative caps a `PadTemplates` type declares on its source pad:
796    /// the first alternative of its first source template, or `None` if it has
797    /// no source pad or only a wildcard one. This is what a g2g source "knows it
798    /// produces" without byte-stream `typefind`, the input an auto-plugged
799    /// decode chain starts from.
800    pub fn declared_source_caps<S: PadTemplates>() -> Option<Caps> {
801        match S::pad_template(PadDirection::Source)?.caps {
802            PadCaps::Fixed(set) => set.alternatives().first().cloned(),
803            PadCaps::Any => None,
804        }
805    }
806
807    /// A registered source element: its declared output caps and a constructor.
808    /// Unlike [`ElementFactory`] (transforms / sinks, which the search composes),
809    /// a source is the *root* of a graph, so it carries its output caps directly
810    /// rather than being matched into a chain. Use [`declared_source_caps`] to
811    /// derive the caps from a [`PadTemplates`] type.
812    pub struct SourceFactory {
813        name: &'static str,
814        output: Caps,
815        build: fn() -> Box<dyn DynSourceLoop>,
816        experimental: bool,
817    }
818
819    impl SourceFactory {
820        /// Register a source by name, its declared output caps, and constructor.
821        pub fn new(
822            name: &'static str,
823            output: Caps,
824            build: fn() -> Box<dyn DynSourceLoop>,
825        ) -> Self {
826            Self {
827                name,
828                output,
829                build,
830                experimental: false,
831            }
832        }
833
834        /// Mark this source experimental: its runtime path is host- or
835        /// device-validated, not a CI promise. `g2g-inspect` prints it.
836        pub fn with_experimental(mut self) -> Self {
837            self.experimental = true;
838            self
839        }
840
841        /// Whether this factory declared [`with_experimental`].
842        pub fn experimental(&self) -> bool {
843            self.experimental
844        }
845    }
846
847    impl core::fmt::Debug for SourceFactory {
848        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
849            f.debug_struct("SourceFactory")
850                .field("name", &self.name)
851                .field("output", &self.output)
852                .finish_non_exhaustive()
853        }
854    }
855
856    /// A parsed URI, split at `://` into a scheme and the remainder. The
857    /// remainder is left uninterpreted: each [`UriSourceFactory`] reads it the
858    /// way its scheme needs (a host:port for `udp://`, a filesystem path for
859    /// `file://`, the whole URI for `rtsp://`). Minimal by design, so core pulls
860    /// no URL-parsing dependency; scheme-specific parsing lives in the handler.
861    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
862    pub struct Uri<'a> {
863        /// The full URI as given, e.g. `rtsp://host:554/stream`.
864        pub raw: &'a str,
865        /// The scheme before `://`, lowercased-by-convention by the caller.
866        pub scheme: &'a str,
867        /// Everything after `://`: authority + path + query, uninterpreted.
868        pub rest: &'a str,
869    }
870
871    impl<'a> Uri<'a> {
872        /// Split `raw` at the first `://`. `None` if there is no `://` or the
873        /// scheme is empty.
874        pub fn parse(raw: &'a str) -> Option<Uri<'a>> {
875            let (scheme, rest) = raw.split_once("://")?;
876            if scheme.is_empty() {
877                return None;
878            }
879            Some(Uri { raw, scheme, rest })
880        }
881    }
882
883    /// Why [`Registry::build_uridecodebin`] could not assemble a graph.
884    #[derive(Debug)]
885    pub enum UriError {
886        /// The URI did not parse as `scheme://rest`, or a handler could not
887        /// interpret its scheme-specific remainder (e.g. a bad `host:port`).
888        Malformed,
889        /// No URI handler is registered for the scheme.
890        UnknownScheme,
891        /// The source's caps could not be decoded to the target (wraps the
892        /// `decodebin` failure).
893        Decode(DecodebinError),
894    }
895
896    impl From<DecodebinError> for UriError {
897        fn from(e: DecodebinError) -> Self {
898            UriError::Decode(e)
899        }
900    }
901
902    /// A URI handler's build function: parse a [`Uri`] into a constructed source
903    /// plus the caps it produces (the `decodebin` input).
904    type UriSourceBuild = fn(&Uri) -> Result<(Box<dyn DynSourceLoop>, Caps), UriError>;
905
906    /// A URI-scheme handler: maps a parsed [`Uri`] to a constructed source and
907    /// the source's declared output caps (the `decodebin` input). The analog of
908    /// GStreamer's `GstURIHandler`. Unlike [`SourceFactory`] (a parameterless
909    /// `playbin` root named directly), this builds the source *from the URI*, so
910    /// `udp://host:port` and `file://path` configure themselves.
911    pub struct UriSourceFactory {
912        scheme: &'static str,
913        build: UriSourceBuild,
914    }
915
916    impl UriSourceFactory {
917        /// Register a handler for `scheme` (e.g. `"rtsp"`, `"udp"`, `"file"`).
918        /// `build` parses the URI's remainder, constructs the source, and
919        /// returns it with the caps it produces.
920        pub fn new(scheme: &'static str, build: UriSourceBuild) -> Self {
921            Self { scheme, build }
922        }
923    }
924
925    impl core::fmt::Debug for UriSourceFactory {
926        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
927            f.debug_struct("UriSourceFactory")
928                .field("scheme", &self.scheme)
929                .finish_non_exhaustive()
930        }
931    }
932
933    /// Why [`Registry::build_playbin`] could not assemble a graph.
934    #[derive(Debug)]
935    pub enum PlaybinError {
936        /// No source is registered under the requested name.
937        UnknownSource,
938        /// The source's caps could not be decoded to the target (wraps the
939        /// `decodebin` failure: no chain, or a graph link error).
940        Decode(DecodebinError),
941    }
942
943    impl From<DecodebinError> for PlaybinError {
944        fn from(e: DecodebinError) -> Self {
945            PlaybinError::Decode(e)
946        }
947    }
948
949    /// One output branch of a `playbin` graph (M379): the elementary stream a
950    /// demux port carries, the raw target to decode it to, and the sink it ends in.
951    /// The caller derives these from the demux's announced
952    /// [`StreamCollection`](crate::stream::StreamCollection) and its selection: one
953    /// `PlaybinPort` per selected stream, in demux port order.
954    pub struct PlaybinPort {
955        /// The port's elementary-stream caps (e.g. H.264), the decode-chain input
956        /// the registry auto-plugs from.
957        pub input_caps: Caps,
958        /// The raw shape to decode the port to (commonly [`is_raw_video`] for a
959        /// video port, [`is_raw_audio`] for an audio port).
960        pub target: Box<dyn Fn(&Caps) -> bool>,
961        /// The terminal sink for this branch (e.g. an `autovideosink` /
962        /// `autoaudiosink` chosen by the stream kind).
963        pub sink: Box<dyn DynAsyncElement>,
964    }
965
966    impl core::fmt::Debug for PlaybinPort {
967        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
968            f.debug_struct("PlaybinPort")
969                .field("input_caps", &self.input_caps)
970                .finish_non_exhaustive()
971        }
972    }
973
974    /// Why [`Registry::build_playbin_graph`] could not assemble a graph.
975    #[derive(Debug)]
976    pub enum PlaybinGraphError {
977        /// No output ports were given (a `playbin` needs at least one stream).
978        NoPorts,
979        /// The URI could not be dispatched to a source (wraps the scheme failure).
980        Uri(UriError),
981        /// Linking the source to the demux failed.
982        Graph(GraphError),
983        /// A port's decode chain could not be spliced (no chain, or a link error).
984        Decode(DecodebinError),
985    }
986
987    impl From<UriError> for PlaybinGraphError {
988        fn from(e: UriError) -> Self {
989            PlaybinGraphError::Uri(e)
990        }
991    }
992    impl From<GraphError> for PlaybinGraphError {
993        fn from(e: GraphError) -> Self {
994            PlaybinGraphError::Graph(e)
995        }
996    }
997    impl From<DecodebinError> for PlaybinGraphError {
998        fn from(e: DecodebinError) -> Self {
999            PlaybinGraphError::Decode(e)
1000        }
1001    }
1002
1003    /// Property assignments for the elements an auto-plug search selects, keyed
1004    /// by factory name: the auto-plug counterpart of a launch line's `key=value`.
1005    /// A caller that never names the chain's elements (that is the point of
1006    /// auto-plug) still needs to reach into them, e.g. `("videoscale", "width",
1007    /// 640)` or `("filesink", "location", "out.mp4")`, so geometry, a device
1008    /// index, or a file path is just a property name, not a special-cased
1009    /// construction parameter.
1010    ///
1011    /// Applied by the `*_with_params` entry points right after the factory builds
1012    /// the element, through the element's
1013    /// [`set_property`](crate::AsyncElement::set_property), so an element only has
1014    /// to expose the knob it already exposes to `gst-launch`. An assignment
1015    /// addressing a factory the search did not select goes unused (the search
1016    /// legitimately picks a different chain); one a *selected* element rejects is
1017    /// an [`AutoplugError::Property`].
1018    #[derive(Debug, Clone, Default)]
1019    pub struct AutoplugParams {
1020        assignments: Vec<(String, String, PropValue)>,
1021    }
1022
1023    impl AutoplugParams {
1024        /// No assignments (auto-plug behaves exactly as the paramless entry points).
1025        pub fn new() -> Self {
1026            Self::default()
1027        }
1028
1029        /// Assign `property = value` on the element built from the factory named
1030        /// `element`. Builder form; assignments apply in the order given.
1031        pub fn set(mut self, element: &str, property: &str, value: PropValue) -> Self {
1032            self.assignments
1033                .push((element.to_string(), property.to_string(), value));
1034            self
1035        }
1036
1037        /// Apply every assignment addressed to the factory named `element` to a
1038        /// just-built instance of it.
1039        fn apply(
1040            &self,
1041            element: &str,
1042            target: &mut dyn DynAsyncElement,
1043        ) -> Result<(), AutoplugError> {
1044            for (name, property, value) in &self.assignments {
1045                if name != element {
1046                    continue;
1047                }
1048                target
1049                    .set_property(property, value.clone())
1050                    .map_err(|source| AutoplugError::Property {
1051                        element: name.clone(),
1052                        property: property.clone(),
1053                        source,
1054                    })?;
1055            }
1056            Ok(())
1057        }
1058    }
1059
1060    /// Why a params-carrying auto-plug entry point
1061    /// ([`Registry::decodebin_with_params`], [`Registry::build_playbin_with_params`],
1062    /// [`Registry::build_playbin_graph_with_params`]) failed. One type across the
1063    /// three, since they share every failure but the entry-specific first step.
1064    #[derive(Debug)]
1065    pub enum AutoplugError {
1066        /// No source is registered under the requested name.
1067        UnknownSource,
1068        /// No output ports were given (a `playbin` needs at least one stream).
1069        NoPorts,
1070        /// No chain of registered elements converts the input caps to the target
1071        /// within the depth bound.
1072        NoChain,
1073        /// The URI could not be dispatched to a source.
1074        Uri(UriError),
1075        /// A graph link failed.
1076        Graph(GraphError),
1077        /// A selected element rejected an [`AutoplugParams`] assignment: an
1078        /// unknown property name, a type mismatch, or an out-of-range value.
1079        Property {
1080            element: String,
1081            property: String,
1082            source: PropError,
1083        },
1084    }
1085
1086    impl From<GraphError> for AutoplugError {
1087        fn from(e: GraphError) -> Self {
1088            AutoplugError::Graph(e)
1089        }
1090    }
1091
1092    impl From<UriError> for AutoplugError {
1093        fn from(e: UriError) -> Self {
1094            AutoplugError::Uri(e)
1095        }
1096    }
1097
1098    impl From<DecodebinError> for AutoplugError {
1099        fn from(e: DecodebinError) -> Self {
1100            match e {
1101                DecodebinError::NoChain => AutoplugError::NoChain,
1102                DecodebinError::Graph(e) => AutoplugError::Graph(e),
1103            }
1104        }
1105    }
1106
1107    /// A `playbin uri=X` auto-fan-out hook (M382): given the registry and the
1108    /// URI, probe the container and assemble a complete multi-stream
1109    /// `source -> demux -> per-stream decode -> auto sink` graph, the auto
1110    /// counterpart of [`Registry::build_playbin_graph`]. `Ok(Some(graph))`
1111    /// handled it; `Ok(None)` declined (e.g. an unprobed scheme or a container
1112    /// the hook does not parse), so [`parse_launch`](crate::runtime::parse_launch)
1113    /// falls back to single-stream `playbin`; `Err` aborts the parse. A plain
1114    /// `fn` pointer, so `Registry` stays `Default` / `Debug`; the plugin crate
1115    /// that owns the container parsing registers it via
1116    /// [`Registry::register_playbin`]. Cross-crate by design: the text DSL lives
1117    /// in core, the Matroska parsing in `g2g-plugins`.
1118    pub type PlaybinHook = fn(&Registry, &str) -> Result<Option<Graph<GraphNode>>, ParseError>;
1119
1120    /// An explicit-demux fan-out hook (M476), the sibling of [`PlaybinHook`] for a
1121    /// named demux element inside a user-authored line
1122    /// (`filesrc location=x.mkv ! matroskademux name=d  d.video_0 ! ...  d.audio_0 ! ...`).
1123    /// Given the demux element name, its upstream file location, and the ordered
1124    /// output-pad requests the line makes, the hook probes the file and builds the
1125    /// multi-output demuxer with one port per request (in request order), or
1126    /// returns `None` to decline (a different hook's container, an unreadable file,
1127    /// or an unsatisfiable request). Cross-crate by design: the DSL lives in core,
1128    /// the container probing in `g2g-plugins`. Registered via
1129    /// [`Registry::register_demux_select`].
1130    pub type DemuxSelectHook = fn(
1131        name: &str,
1132        location: &str,
1133        pads: &[PadRequest],
1134    ) -> Option<Box<dyn DynMultiOutputElement>>;
1135
1136    /// A `decodebin` fan-out hook (M482): the decode-per-port sibling of
1137    /// [`DemuxSelectHook`], for `filesrc location=x.mkv ! decodebin name=d
1138    /// d.video_0 ! ...  d.audio_0 ! ...`. Unlike the demux-select hook it is NOT
1139    /// keyed by an element name (a `decodebin` names no container): each registered
1140    /// hook probes the file and returns `Some` only for the container it parses, so
1141    /// the parser tries them in turn. It returns the multi-output demuxer plus each
1142    /// selected port's elementary caps, so the parser can auto-plug a decoder chain
1143    /// onto every port (the demuxer emits elementary streams; `decodebin` decodes
1144    /// them to raw). `None` declines (a different container, an unreadable file, or
1145    /// an unsatisfiable request). Registered via [`Registry::register_decodebin_select`].
1146    pub type DecodebinSelectHook = fn(
1147        location: &str,
1148        pads: &[PadRequest],
1149    ) -> Option<(Box<dyn DynMultiOutputElement>, Vec<Caps>)>;
1150
1151    /// The single-stream demux + decode chain a bare (non-fan-out) `decodebin`
1152    /// should build for a file-backed container whose default single-stream demux
1153    /// guesses the wrong port (M746). A single-stream demuxer fixes its output pad
1154    /// at negotiation before parsing any byte, so it defaults to a video port; an
1155    /// audio-only MPEG-TS then auto-plugs a video decoder and fails "no caps
1156    /// overlap". `expand_decodebin` consults a hook so the demux instead selects the
1157    /// container's real (audio) stream, building `filesrc ! demux stream=X !
1158    /// <decoder> ! ...`.
1159    #[derive(Debug, Clone)]
1160    pub struct PrimaryStream {
1161        /// The single-stream demux element to plug (e.g. `"tsdemux"`).
1162        pub demux: &'static str,
1163        /// Properties that select the primary stream on `demux` (e.g.
1164        /// `[("stream", "aac")]`).
1165        pub props: Vec<(String, String)>,
1166        /// The elementary caps the demux emits for that stream, the auto-plug input
1167        /// for the decoder chain spliced after it.
1168        pub caps: Caps,
1169    }
1170
1171    /// A bare-`decodebin` primary-stream hook (M746): given the upstream file
1172    /// location and the container caps, sniff the container and return the
1173    /// single-stream demux + stream selection for its primary decodable stream, or
1174    /// `None` when the default video port is correct (the container derives its
1175    /// output caps from the file, or the sniffed video codec matches the port
1176    /// default), the file is unreadable, or the hook does not parse the container.
1177    /// A demux whose port caps are fixed before parsing (TsDemux, M936) must name
1178    /// the stream even when a video track is present. Registered via
1179    /// [`Registry::register_primary_stream`].
1180    pub type PrimaryStreamHook = fn(location: &str, caps: &Caps) -> Option<PrimaryStream>;
1181
1182    impl core::fmt::Debug for ElementFactory {
1183        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1184            f.debug_struct("ElementFactory")
1185                .field("name", &self.desc.name)
1186                .finish_non_exhaustive()
1187        }
1188    }
1189
1190    /// The memory domain a consumer accepting `accepted` wants its frames in: its
1191    /// most-preferred domain (GPU-resident before `System`, per `DomainSet`).
1192    /// [`DomainSet::ALL`] is the default an element that never declared carries,
1193    /// meaning "imposes no requirement" rather than "wants any domain", so it
1194    /// derives `System` and leaves a plain pipeline's selection alone.
1195    fn memory_preference(accepted: DomainSet) -> MemoryDomainKind {
1196        if accepted == DomainSet::ALL {
1197            return MemoryDomainKind::System;
1198        }
1199        accepted.preferred().unwrap_or(MemoryDomainKind::System)
1200    }
1201
1202    /// A runtime collection of element factories the auto-plugger searches over,
1203    /// the analog of GStreamer's plugin registry. Registration order is the
1204    /// tie-break only indirectly: [`find_chain`] is breadth-first, so among
1205    /// equal-length chains the one whose elements register earliest is found
1206    /// first.
1207    #[derive(Debug, Default)]
1208    pub struct Registry {
1209        factories: Vec<ElementFactory>,
1210        sources: Vec<SourceFactory>,
1211        uris: Vec<UriSourceFactory>,
1212        launch: Vec<LaunchFactory>,
1213        muxers: Vec<MuxerFactory>,
1214        demuxes: Vec<DemuxFactory>,
1215        fanout_srcs: Vec<FanoutSrcFactory>,
1216        /// gst-canonical-name aliases (M192): each maps a name to an ordered list
1217        /// of registered targets, the first that is actually registered wins. A
1218        /// plain rename is a one-entry list; `autovideosink` is a fallback chain
1219        /// (`waylandsink`, `kmssink`, ..., `fakesink`). Resolved at `make_*` time,
1220        /// so an alias whose targets are all feature-gated-out simply misses.
1221        aliases: Vec<(&'static str, &'static [&'static str])>,
1222        /// The `playbin uri=X` auto-fan-out hooks (M382, multi-hook M389). A lone
1223        /// `playbin` in a text pipeline tries each in registration order until one
1224        /// handles the URI (`Ok(Some)`); each declines (`Ok(None)`) a container it
1225        /// does not parse, so one hook per container type coexists (MKV, TS, ...).
1226        /// Empty (the default) leaves `playbin` as the M196 single-stream pipeline.
1227        playbin: Vec<PlaybinHook>,
1228        /// Explicit-demux fan-out hooks (M476): a named demux element with several
1229        /// output-pad references and a file source upstream tries each in order
1230        /// until one builds the multi-output demuxer (`Some`); each declines
1231        /// (`None`) a container it does not parse. Empty (the default) leaves a
1232        /// demux element single-stream (its M114/M180 launch registration).
1233        demux_select: Vec<DemuxSelectHook>,
1234        /// `decodebin` fan-out hooks (M482): the decode-per-port sibling of
1235        /// `demux_select`. A `decodebin name=d` with several `d.` refs and a file
1236        /// upstream tries each until one parses the container, returning the
1237        /// demuxer + per-port caps so the parser splices a decoder onto each port.
1238        decodebin_select: Vec<DecodebinSelectHook>,
1239        /// Bare-`decodebin` primary-stream hooks (M746): a `filesrc location=X !
1240        /// decodebin` on a container tries each until one sniffs the file and names
1241        /// the single-stream demux + stream selection for its primary decodable
1242        /// stream. Empty (the default) keeps the demux's declared default port, so
1243        /// an audio-only container plugs the wrong (video) decoder.
1244        primary_stream: Vec<PrimaryStreamHook>,
1245        /// Optional parser injector (M421): given a decode-chain input caps,
1246        /// returns a parser element to splice in just before the decoder (e.g. an
1247        /// access-unit-re-framing `h264parse` for `CompressedVideo { H264, .. }`),
1248        /// or `None` to decode the input directly. A decoder fed un-aligned units
1249        /// (one MPEG-TS PES that is not one access unit) mis-parses; this is how
1250        /// the registry mirrors GStreamer's `decodebin` always inserting a parser.
1251        /// Maps input caps to the LAUNCH NAME of the parser (M676: a name, not a
1252        /// boxed element, so the name-based `decodebin` expansion in `parse_launch`
1253        /// shares the one mapping); the element is built via
1254        /// [`make_element`](Self::make_element). A bare function pointer so the
1255        /// registry stays `Debug` + `Default`; [`default_registry`](crate) sets it.
1256        /// `None` (the default) decodes directly, preserving the prior behaviour
1257        /// for registries that do not set it.
1258        parser_provider: Option<fn(&Caps) -> Option<&'static str>>,
1259        /// Encoder candidates for a coded target caps, in preference order
1260        /// (hardware before software), consulted by the `encodebin` expansion.
1261        encoder_provider: Option<EncoderProvider>,
1262        /// Muxer candidates for a container caps, in preference order.
1263        muxer_provider: Option<MuxerProvider>,
1264        /// The memory-domain converter factory `parse_launch` splices with (M354,
1265        /// M1017). The converter elements live outside this crate, so the registry
1266        /// carries the factory; `None` (the default) leaves a domain mismatch to
1267        /// fail loud at negotiation, as it did before.
1268        domain_converter: Option<fn(MemoryDomainKind, MemoryDomainKind) -> Option<GraphNode>>,
1269    }
1270
1271    impl Registry {
1272        /// An empty registry.
1273        pub fn new() -> Self {
1274            Self::default()
1275        }
1276
1277        /// Register one element factory (a transform / sink the search composes
1278        /// into chains), returning `&mut self` to chain calls.
1279        pub fn register(&mut self, factory: ElementFactory) -> &mut Self {
1280            self.factories.push(factory);
1281            self
1282        }
1283
1284        /// Register one source factory (a graph root for [`build_playbin`]),
1285        /// returning `&mut self` to chain calls.
1286        pub fn register_source(&mut self, source: SourceFactory) -> &mut Self {
1287            self.sources.push(source);
1288            self
1289        }
1290
1291        /// Register one URI-scheme handler (a graph root for
1292        /// [`build_uridecodebin`](Self::build_uridecodebin)), returning
1293        /// `&mut self` to chain calls.
1294        pub fn register_uri(&mut self, handler: UriSourceFactory) -> &mut Self {
1295            self.uris.push(handler);
1296            self
1297        }
1298
1299        /// Register a named transform / sink for the `gst-launch` parser and
1300        /// `gst-inspect` (M105), returning `&mut self` to chain calls.
1301        pub fn register_launch(&mut self, factory: LaunchFactory) -> &mut Self {
1302            self.launch.push(factory);
1303            self
1304        }
1305
1306        /// Register a named fan-in muxer for the `gst-launch` parser (M122),
1307        /// returning `&mut self` to chain calls.
1308        pub fn register_muxer(&mut self, factory: MuxerFactory) -> &mut Self {
1309            self.muxers.push(factory);
1310            self
1311        }
1312
1313        /// Register a named fan-out demuxer for the `gst-launch` parser (M210),
1314        /// returning `&mut self` to chain calls.
1315        pub fn register_fanout_src(&mut self, factory: FanoutSrcFactory) -> &mut Self {
1316            self.fanout_srcs.push(factory);
1317            self
1318        }
1319
1320        /// Whether `name` is a registered terminal fan-out source.
1321        pub fn is_fanout_src(&self, name: &str) -> bool {
1322            self.fanout_srcs.iter().any(|f| f.name == name)
1323        }
1324
1325        /// Build a registered terminal fan-out source with `outputs` ports.
1326        pub fn make_fanout_src(
1327            &self,
1328            name: &str,
1329            outputs: usize,
1330        ) -> Option<Box<dyn crate::fanout::DynMultiOutputSource>> {
1331            self.fanout_srcs
1332                .iter()
1333                .find(|f| f.name == name)
1334                .map(|f| (f.build)(outputs))
1335        }
1336
1337        pub fn register_demux(&mut self, factory: DemuxFactory) -> &mut Self {
1338            self.demuxes.push(factory);
1339            self
1340        }
1341
1342        /// Register a `playbin uri=X` auto-fan-out hook (M382): a lone `playbin`
1343        /// in a [`parse_launch`](crate::runtime::parse_launch) pipeline tries the
1344        /// registered hooks in order until one handles the URI. Register one per
1345        /// container type (MKV, TS, ...); each declines a container it does not
1346        /// parse. Returns `&mut self` to chain calls.
1347        pub fn register_playbin(&mut self, hook: PlaybinHook) -> &mut Self {
1348            self.playbin.push(hook);
1349            self
1350        }
1351
1352        /// The registered explicit-demux fan-out hooks (M476), tried in order by
1353        /// [`parse_launch`](crate::runtime::parse_launch) for a named demux element
1354        /// with several output-pad references and a file source upstream.
1355        pub fn demux_select_hooks(&self) -> &[DemuxSelectHook] {
1356            &self.demux_select
1357        }
1358
1359        /// Register an explicit-demux fan-out hook (M476): a named demux element
1360        /// (`matroskademux name=d  d.video_0 ! ...  d.audio_0 ! ...`) fed by a file
1361        /// source tries the registered hooks in order until one probes the file and
1362        /// builds the multi-output demuxer. Register one per container type; each
1363        /// declines a container it does not parse. Returns `&mut self` to chain.
1364        pub fn register_demux_select(&mut self, hook: DemuxSelectHook) -> &mut Self {
1365            self.demux_select.push(hook);
1366            self
1367        }
1368
1369        /// The `decodebin` fan-out hooks (M482), tried in order by
1370        /// [`parse_launch`](crate::runtime::parse_launch) for a `decodebin name=d`
1371        /// with several `d.` references and a file source upstream.
1372        pub fn decodebin_select_hooks(&self) -> &[DecodebinSelectHook] {
1373            &self.decodebin_select
1374        }
1375
1376        /// Register a `decodebin` fan-out hook (M482): a `decodebin name=d` fed by a
1377        /// file source tries the registered hooks until one parses the container,
1378        /// returning the multi-output demuxer + per-port caps so the parser splices
1379        /// a decoder onto each port. One per container type. Returns `&mut self`.
1380        pub fn register_decodebin_select(&mut self, hook: DecodebinSelectHook) -> &mut Self {
1381            self.decodebin_select.push(hook);
1382            self
1383        }
1384
1385        /// Register a bare-`decodebin` primary-stream hook (M746): a `filesrc
1386        /// location=X ! decodebin` on a container tries each until one sniffs the
1387        /// file and names the single-stream demux + stream selection for its primary
1388        /// decodable stream. One per container type. Returns `&mut self`.
1389        pub fn register_primary_stream(&mut self, hook: PrimaryStreamHook) -> &mut Self {
1390            self.primary_stream.push(hook);
1391            self
1392        }
1393
1394        /// The single-stream demux + decode selection for a bare `decodebin` on the
1395        /// container at `location` with `caps`, from the first hook that parses it
1396        /// (M746); `None` when none applies (no video-less container to fix, an
1397        /// unreadable file, or no hook registered).
1398        pub fn primary_stream(&self, location: &str, caps: &Caps) -> Option<PrimaryStream> {
1399            self.primary_stream
1400                .iter()
1401                .find_map(|hook| hook(location, caps))
1402        }
1403
1404        /// Set the parser injector consulted by [`decodebin`](Self::decodebin) and
1405        /// [`decodebin_preferring`](Self::decodebin_preferring) (M421): before a
1406        /// decode chain is spliced, `provider(input)` may name a registered parser
1407        /// (e.g. an access-unit-re-framing `h264parse`) to prepend ahead of the
1408        /// decoder, so the decoder is fed one access unit per packet. The
1409        /// name-based `decodebin` expansion in `parse_launch` consults the same
1410        /// mapping via [`parser_name`](Self::parser_name) (M676). Returns
1411        /// `&mut self` to chain calls.
1412        pub fn set_parser_provider(
1413            &mut self,
1414            provider: fn(&Caps) -> Option<&'static str>,
1415        ) -> &mut Self {
1416            self.parser_provider = Some(provider);
1417            self
1418        }
1419
1420        /// Set the encoder chooser consulted by the `encodebin` expansion: for a
1421        /// coded target caps (a stream of an encoding profile), `provider(target)`
1422        /// names the launch elements that can produce it, most preferred first, and
1423        /// [`encoder_name`](Self::encoder_name) takes the first one this build
1424        /// actually registered. Returns `&mut self` to chain calls.
1425        pub fn set_encoder_provider(&mut self, provider: EncoderProvider) -> &mut Self {
1426            self.encoder_provider = Some(provider);
1427            self
1428        }
1429
1430        /// Set the muxer chooser consulted by the `encodebin` expansion: for a
1431        /// container caps, `provider(container)` names the muxers that write it,
1432        /// most preferred first. Returns `&mut self` to chain calls.
1433        pub fn set_muxer_provider(&mut self, provider: MuxerProvider) -> &mut Self {
1434            self.muxer_provider = Some(provider);
1435            self
1436        }
1437
1438        /// The encoder that produces `target`: the first candidate this build
1439        /// registered and can run, with the properties that pin it to this codec.
1440        /// `None` when no provider is set, the target has no candidates, or every
1441        /// candidate is compiled out (the "no encoder for this profile" a caller
1442        /// reports).
1443        pub fn encoder_choice(&self, target: &Caps) -> Option<&'static EncoderChoice> {
1444            let candidates = self
1445                .encoder_provider
1446                .and_then(|provider| provider(target))?;
1447            candidates
1448                .iter()
1449                .find(|choice| self.has_usable_launch(choice.element))
1450        }
1451
1452        /// The launch name of the muxer that writes `container`, chosen the way
1453        /// [`encoder_name`](Self::encoder_name) chooses an encoder.
1454        pub fn muxer_name(&self, container: &Caps) -> Option<&'static str> {
1455            let candidates = self
1456                .muxer_provider
1457                .and_then(|provider| provider(container))?;
1458            candidates.iter().copied().find(|name| {
1459                self.has_usable_launch(name) || self.muxers.iter().any(|m| m.name == *name)
1460            })
1461        }
1462
1463        /// Whether a launch factory of this name is registered and its
1464        /// `with_usable` check (if any) passes here.
1465        fn has_usable_launch(&self, name: &str) -> bool {
1466            self.launch
1467                .iter()
1468                .any(|factory| factory.name == name && factory.usable())
1469        }
1470
1471        /// Set the memory-domain converter factory a parsed pipeline is spliced
1472        /// with (M1017): after the graph is built, an edge whose producer and
1473        /// consumer share no memory domain gets `factory(from, to)` inserted, so a
1474        /// text pipeline reaches a GPU sink from a GPU decoder without naming the
1475        /// bridge element. Returns `&mut self` to chain calls.
1476        pub fn set_domain_converter(
1477            &mut self,
1478            factory: fn(MemoryDomainKind, MemoryDomainKind) -> Option<GraphNode>,
1479        ) -> &mut Self {
1480            self.domain_converter = Some(factory);
1481            self
1482        }
1483
1484        /// The configured domain-converter factory, if any.
1485        pub fn domain_converter(
1486            &self,
1487        ) -> Option<fn(MemoryDomainKind, MemoryDomainKind) -> Option<GraphNode>> {
1488            self.domain_converter
1489        }
1490
1491        /// The launch name of the re-framing parser `decodebin` prepends ahead of
1492        /// a decoder for `input` caps, if the provider names one (M676).
1493        pub fn parser_name(&self, input: &Caps) -> Option<&'static str> {
1494            self.parser_provider.and_then(|provider| provider(input))
1495        }
1496
1497        /// Prepend the [`parser_provider`](Self::set_parser_provider)'s parser to a
1498        /// just-autoplugged decode chain, when one is configured and the chain is a
1499        /// real decode (non-empty: an already-satisfied input needs no parser).
1500        /// Returns the launch name of the parser it prepended, so a caller with
1501        /// [`AutoplugParams`] can address it like any other auto-plugged element.
1502        fn maybe_prepend_parser(
1503            &self,
1504            input: &Caps,
1505            elements: &mut Vec<Box<dyn DynAsyncElement>>,
1506        ) -> Option<&'static str> {
1507            if elements.is_empty() {
1508                return None;
1509            }
1510            let name = self.parser_name(input)?;
1511            let parser = self.make_element(name)?;
1512            elements.insert(0, parser);
1513            Some(name)
1514        }
1515
1516        /// The registered [`PlaybinHook`]s, in registration order (empty if none,
1517        /// so `playbin` stays the M196 single-stream pipeline). The parser tries
1518        /// them in turn for a lone `playbin uri=`.
1519        pub fn playbin_hooks(&self) -> &[PlaybinHook] {
1520            &self.playbin
1521        }
1522
1523        /// Register a gst-canonical-name alias (M192): `name` resolves, at
1524        /// `make_source` / `make_element` time, to the first of `targets` that is
1525        /// actually registered. Use a one-entry list for a plain rename
1526        /// (`avdec_h264` -> `ffmpegdec`) and a fallback chain for an auto element
1527        /// (`autovideosink` -> `["waylandsink", "kmssink", "fakesink"]`). Returns
1528        /// `&mut self` to chain calls.
1529        pub fn register_alias(
1530            &mut self,
1531            name: &'static str,
1532            targets: &'static [&'static str],
1533        ) -> &mut Self {
1534            self.aliases.push((name, targets));
1535            self
1536        }
1537
1538        /// Resolve a name through the alias table to the first target that is
1539        /// registered and can run here, or the name itself when it is not an
1540        /// alias. One hop only (aliases do not chain to other aliases).
1541        ///
1542        /// A launch target that declared a
1543        /// [`with_usable`](LaunchFactory::with_usable) check is skipped when the
1544        /// check says no, so `autovideosink` falls past a display sink that was
1545        /// compiled in but has no display to present on. The last entry of an
1546        /// auto alias is `fakesink`, which always runs.
1547        pub(crate) fn resolve_alias<'a>(&self, name: &'a str) -> &'a str {
1548            if let Some((_, targets)) = self.aliases.iter().find(|(a, _)| *a == name) {
1549                for &t in *targets {
1550                    if let Some(factory) = self.launch.iter().find(|f| f.name == t) {
1551                        if factory.usable() {
1552                            return t;
1553                        }
1554                        continue;
1555                    }
1556                    if self.sources.iter().any(|s| s.name == t)
1557                        || self.muxers.iter().any(|m| m.name == t)
1558                        || self.demuxes.iter().any(|d| d.name == t)
1559                    {
1560                        return t;
1561                    }
1562                }
1563            }
1564            name
1565        }
1566
1567        /// Construct a registered source by name (the parser's first element).
1568        /// `None` if no source is registered under `name` (after alias resolution).
1569        pub fn make_source(&self, name: &str) -> Option<Box<dyn DynSourceLoop>> {
1570            let name = self.resolve_alias(name);
1571            self.sources
1572                .iter()
1573                .find(|s| s.name == name)
1574                .map(|s| (s.build)())
1575        }
1576
1577        /// Construct a registered transform / sink by name (a parser interior or
1578        /// tail element), default-configured. `None` if `name` is not registered
1579        /// via [`register_launch`](Self::register_launch) (after alias resolution).
1580        pub fn make_element(&self, name: &str) -> Option<Box<dyn DynAsyncElement>> {
1581            let name = self.resolve_alias(name);
1582            self.launch
1583                .iter()
1584                .find(|f| f.name == name)
1585                .map(|f| (f.build)())
1586        }
1587
1588        /// The caps a registered element is known to produce on its source pad,
1589        /// without constructing or negotiating it: a source's declared output, or
1590        /// a transform / sink's first fixed source-pad template alternative.
1591        /// `None` for an unregistered name or one whose source pad is wildcard.
1592        /// The `decodebin` parser uses this to learn its upstream caps (the input
1593        /// to the auto-plug search). Reads the factory-declared media type; it does
1594        /// not reflect instance properties that re-type the output (e.g. a
1595        /// `filesrc`'s `bytestream-format`).
1596        pub fn declared_output_caps(&self, name: &str) -> Option<Caps> {
1597            let name = self.resolve_alias(name);
1598            if let Some(s) = self.sources.iter().find(|s| s.name == name) {
1599                return Some(s.output.clone());
1600            }
1601            let f = self.launch.iter().find(|f| f.name == name)?;
1602            let t = f
1603                .templates
1604                .iter()
1605                .find(|t| t.direction == PadDirection::Source)?;
1606            match &t.caps {
1607                PadCaps::Fixed(set) => set.alternatives().first().cloned(),
1608                PadCaps::Any => None,
1609            }
1610        }
1611
1612        /// Construct a registered fan-in muxer by name with `inputs` input pads
1613        /// (the parser derives the count from link degree, so it matches the
1614        /// muxer node's input-pad count). `None` if `name` is not registered via
1615        /// [`register_muxer`](Self::register_muxer).
1616        pub fn make_muxer(
1617            &self,
1618            name: &str,
1619            inputs: usize,
1620        ) -> Option<Box<dyn DynMultiInputElement>> {
1621            let name = self.resolve_alias(name);
1622            self.muxers
1623                .iter()
1624                .find(|m| m.name == name)
1625                .map(|m| (m.build)(inputs))
1626        }
1627
1628        /// Construct a registered fan-out demuxer by name with `outputs` output
1629        /// pads (the parser derives the count from the `d.` link degree, so it
1630        /// matches the demux node's fan-out). `None` if `name` is not registered
1631        /// via [`register_demux`](Self::register_demux).
1632        pub fn make_demux(
1633            &self,
1634            name: &str,
1635            outputs: usize,
1636        ) -> Option<Box<dyn DynMultiOutputElement>> {
1637            let name = self.resolve_alias(name);
1638            self.demuxes
1639                .iter()
1640                .find(|d| d.name == name)
1641                .map(|d| (d.build)(outputs))
1642        }
1643
1644        /// Whether `name` is registered as a fan-out demuxer (the parser uses this
1645        /// to allow a multi-output node without an explicit `tee`).
1646        pub fn is_demux(&self, name: &str) -> bool {
1647            self.demuxes.iter().any(|d| d.name == name)
1648        }
1649
1650        /// The names of every element registerable by the parser: sources first,
1651        /// then transforms / sinks, each in registration order. The `gst-inspect`
1652        /// element list.
1653        /// Whether `name` names something this registry can build: any factory
1654        /// list, or an alias resolving into one. The launch parser asks this to
1655        /// tell a new chain's head from a typo'd property, since both are bare
1656        /// tokens with no `=`.
1657        pub fn knows_element(&self, name: &str) -> bool {
1658            let name = self.resolve_alias(name);
1659            self.sources.iter().any(|s| s.name == name)
1660                || self.launch.iter().any(|f| f.name == name)
1661                || self.muxers.iter().any(|m| m.name == name)
1662                || self.demuxes.iter().any(|d| d.name == name)
1663                || self.fanout_srcs.iter().any(|f| f.name == name)
1664                || self.factories.iter().any(|f| f.desc.name == name)
1665        }
1666
1667        pub fn element_names(&self) -> Vec<&'static str> {
1668            self.sources
1669                .iter()
1670                .map(|s| s.name)
1671                .chain(self.launch.iter().map(|f| f.name))
1672                // A muxer dual-registered as a launch element (e.g. `mp4mux`) is
1673                // already listed above; only emit muxer-only names here.
1674                .chain(
1675                    self.muxers
1676                        .iter()
1677                        .map(|m| m.name)
1678                        .filter(|name| !self.launch.iter().any(|f| f.name == *name)),
1679                )
1680                .collect()
1681        }
1682
1683        /// One line per registerable element, `name: Long-name` (the long name
1684        /// from the element's [`metadata`](crate::AsyncElement::metadata), or just
1685        /// the name when it declares none), for the `gst-inspect` element index.
1686        /// Sources, then transforms / sinks, then muxers. Each non-muxer element is
1687        /// default-built to read its metadata (side-effect-free, like
1688        /// [`inspect`](Self::inspect)).
1689        pub fn element_listing(&self) -> Vec<String> {
1690            use alloc::string::ToString;
1691            let line = |name: &str, long: &str, experimental: bool| {
1692                let mut s = if long.is_empty() {
1693                    name.to_string()
1694                } else {
1695                    let mut s = name.to_string();
1696                    s.push_str(": ");
1697                    s.push_str(long);
1698                    s
1699                };
1700                if experimental {
1701                    s.push_str(" [experimental]");
1702                }
1703                s
1704            };
1705            let mut lines = Vec::new();
1706            for s in &self.sources {
1707                lines.push(line(
1708                    s.name,
1709                    (s.build)().metadata().long_name,
1710                    s.experimental,
1711                ));
1712            }
1713            for f in &self.launch {
1714                lines.push(line(
1715                    f.name,
1716                    (f.build)().metadata().long_name,
1717                    f.experimental,
1718                ));
1719            }
1720            for m in &self.muxers {
1721                // Skip a muxer already listed as a launch element above. A
1722                // one-input instance carries the metadata, the way `inspect`
1723                // reads a muxer's.
1724                if !self.launch.iter().any(|f| f.name == m.name) {
1725                    lines.push(line(m.name, (m.build)(1).metadata().long_name, false));
1726                }
1727            }
1728            lines
1729        }
1730
1731        /// A `gst-inspect`-style dump for the named element: its role, its
1732        /// settable properties, and (for a transform / sink) its pad templates.
1733        /// `None` if the name is not registered. The element is default-built to
1734        /// read its property table (the specs are `&'static`, behind an instance
1735        /// method), so building must be side-effect-free, as the in-tree
1736        /// constructors are.
1737        pub fn inspect(&self, name: &str) -> Option<String> {
1738            use crate::property::format_metadata;
1739            use core::fmt::Write;
1740            let mut out = String::new();
1741            if let Some(s) = self.sources.iter().find(|s| s.name == name) {
1742                let src = (s.build)();
1743                out.push_str(&format_metadata(name, &src.metadata()));
1744                let _ = writeln!(out, "  Role        source");
1745                if s.experimental {
1746                    let _ = writeln!(out, "  Stability   experimental");
1747                }
1748                let _ = writeln!(out, "\nOutput caps:\n  {:?}", s.output);
1749                let _ = write!(
1750                    out,
1751                    "\nElement Properties:\n{}",
1752                    format_specs(src.properties())
1753                );
1754                Some(out)
1755            } else if let Some(f) = self.launch.iter().find(|f| f.name == name) {
1756                let el = (f.build)();
1757                out.push_str(&format_metadata(name, &el.metadata()));
1758                let _ = writeln!(out, "  Role        element");
1759                if f.experimental {
1760                    let _ = writeln!(out, "  Stability   experimental");
1761                }
1762                let _ = write!(out, "\nPad Templates:\n{}", format_templates(&f.templates));
1763                let _ = write!(
1764                    out,
1765                    "\nElement Properties:\n{}",
1766                    format_specs(el.properties())
1767                );
1768                Some(out)
1769            } else if let Some(m) = self.muxers.iter().find(|m| m.name == name) {
1770                // Build a one-input instance to read its metadata / properties (the
1771                // specs are `&'static`, behind instance methods); the fan-in
1772                // constructors are side-effect-free, so this is safe. The real input
1773                // count is derived from link degree at parse, not needed here.
1774                let mux = (m.build)(1);
1775                out.push_str(&format_metadata(name, &mux.metadata()));
1776                let _ = writeln!(out, "  Role        muxer (fan-in)");
1777                if let Ok(caps) = mux.output_caps() {
1778                    let _ = writeln!(out, "\nOutput caps:\n  {caps:?}");
1779                }
1780                let _ = writeln!(out, "\nInputs: derived from link degree");
1781                let _ = write!(
1782                    out,
1783                    "\nElement Properties:\n{}",
1784                    format_specs(mux.properties())
1785                );
1786                Some(out)
1787            } else {
1788                None
1789            }
1790        }
1791
1792        /// Structured introspection: the same facts [`inspect`](Self::inspect)
1793        /// dumps as text, returned as an [`ElementDoc`] for tooling to render
1794        /// (e.g. the searchable element reference the docs site is generated from).
1795        /// `None` if the name is not registered. Like `inspect`, it default-builds
1796        /// the element to read its `&'static` metadata / property table, so the
1797        /// in-tree constructors must be side-effect-free.
1798        pub fn describe(&self, name: &str) -> Option<ElementDoc> {
1799            if let Some(s) = self.sources.iter().find(|s| s.name == name) {
1800                let src = (s.build)();
1801                let m = src.metadata();
1802                Some(ElementDoc {
1803                    name: name.to_string(),
1804                    long_name: m.long_name.to_string(),
1805                    klass: m.klass.to_string(),
1806                    description: m.description.to_string(),
1807                    author: m.author.to_string(),
1808                    role: "source".to_string(),
1809                    experimental: s.experimental,
1810                    caps: Some(format!("{:?}", s.output)),
1811                    pads: Vec::new(),
1812                    properties: property_docs(src.properties()),
1813                })
1814            } else if let Some(f) = self.launch.iter().find(|f| f.name == name) {
1815                let el = (f.build)();
1816                let m = el.metadata();
1817                Some(ElementDoc {
1818                    name: name.to_string(),
1819                    long_name: m.long_name.to_string(),
1820                    klass: m.klass.to_string(),
1821                    description: m.description.to_string(),
1822                    author: m.author.to_string(),
1823                    role: "element".to_string(),
1824                    experimental: f.experimental,
1825                    caps: None,
1826                    pads: pad_docs(&f.templates),
1827                    properties: property_docs(el.properties()),
1828                })
1829            } else if let Some(mx) = self.muxers.iter().find(|m| m.name == name) {
1830                let mux = (mx.build)(1);
1831                let m = mux.metadata();
1832                Some(ElementDoc {
1833                    name: name.to_string(),
1834                    long_name: m.long_name.to_string(),
1835                    klass: m.klass.to_string(),
1836                    description: m.description.to_string(),
1837                    author: m.author.to_string(),
1838                    role: "muxer (fan-in)".to_string(),
1839                    experimental: false,
1840                    caps: mux.output_caps().ok().map(|c| format!("{c:?}")),
1841                    pads: Vec::new(),
1842                    properties: property_docs(mux.properties()),
1843                })
1844            } else {
1845                None
1846            }
1847        }
1848
1849        /// Every registered element as an [`ElementDoc`], in the same order as
1850        /// [`element_names`](Self::element_names) (sources, transforms / sinks,
1851        /// then muxer-only names). The structured catalog behind `g2g-docgen`.
1852        pub fn describe_all(&self) -> Vec<ElementDoc> {
1853            self.element_names()
1854                .into_iter()
1855                .filter_map(|n| self.describe(n))
1856                .collect()
1857        }
1858
1859        /// The descriptors of every registered factory, in registration order,
1860        /// indexed identically to the [`find_chain`] result.
1861        fn descs(&self) -> Vec<ElementDesc> {
1862            self.factories.iter().map(|f| f.desc.clone()).collect()
1863        }
1864
1865        /// The descriptors the search may pick from with `avoided` factories left
1866        /// out, plus each one's index into [`descs`](Self::descs) (a chain link
1867        /// names a factory by that index, which leaving entries out would shift).
1868        fn descs_avoiding(&self, avoided: &[&str]) -> (Vec<ElementDesc>, Vec<usize>) {
1869            self.factories
1870                .iter()
1871                .enumerate()
1872                .filter(|(_, f)| !avoided.contains(&f.desc.name))
1873                .map(|(i, f)| (f.desc.clone(), i))
1874                .unzip()
1875        }
1876
1877        /// The names of the shortest chain that avoids every factory in
1878        /// `avoided`: the retry after one of them turned out not to decode the
1879        /// stream, where the search would otherwise pick it again. An empty
1880        /// `avoided` is [`autoplug_names_preferring`](Self::autoplug_names_preferring).
1881        pub fn autoplug_names_avoiding(
1882            &self,
1883            input: &Caps,
1884            target: &dyn Fn(&Caps) -> bool,
1885            max_depth: usize,
1886            preferred: MemoryDomainKind,
1887            avoided: &[&str],
1888        ) -> Option<Vec<&'static str>> {
1889            let (descs, indices) = self.descs_avoiding(avoided);
1890            let chain = find_chain_preferring(&descs, input, target, max_depth, preferred)?;
1891            Some(
1892                chain
1893                    .into_iter()
1894                    .map(|link| self.factories[indices[link.index]].desc.name)
1895                    .collect(),
1896            )
1897        }
1898
1899        /// [`autoplug_names_avoiding`](Self::autoplug_names_avoiding), instantiated.
1900        pub fn autoplug_avoiding(
1901            &self,
1902            input: &Caps,
1903            target: &dyn Fn(&Caps) -> bool,
1904            max_depth: usize,
1905            preferred: MemoryDomainKind,
1906            avoided: &[&str],
1907        ) -> Option<Vec<Box<dyn DynAsyncElement>>> {
1908            let (descs, indices) = self.descs_avoiding(avoided);
1909            let chain = find_chain_preferring(&descs, input, target, max_depth, preferred)?;
1910            let chain = chain
1911                .into_iter()
1912                .map(|link| ChainLink {
1913                    index: indices[link.index],
1914                    output: link.output,
1915                })
1916                .collect();
1917            self.instantiate(chain, &AutoplugParams::new()).ok()
1918        }
1919
1920        /// The factory whose element the runner would name `instance` (its type's
1921        /// log category plus a number, e.g. `VulkanVideoDec0` -> `vulkanvideodec`).
1922        /// The bus names a failing element by instance; acting on it per factory,
1923        /// to auto-plug around the one that failed, needs the way back.
1924        ///
1925        /// Each candidate is default-constructed to be asked its category, which
1926        /// costs an allocation and opens nothing (a decoder reaches its device at
1927        /// `configure_pipeline`). `None` if no registered factory matches.
1928        pub fn factory_of_instance(&self, instance: &str) -> Option<&'static str> {
1929            let category = instance.trim_end_matches(|c: char| c.is_ascii_digit());
1930            if category.is_empty() {
1931                return None;
1932            }
1933            self.launch
1934                .iter()
1935                .find(|f| (f.build)().log_category() == category)
1936                .map(|f| f.name)
1937        }
1938
1939        /// The names of the shortest chain converting `input` into caps
1940        /// satisfying `target`, without instantiating anything. `Some(vec![])`
1941        /// if `input` already satisfies `target`; `None` if no chain exists
1942        /// within `max_depth`.
1943        pub fn autoplug_names(
1944            &self,
1945            input: &Caps,
1946            target: &dyn Fn(&Caps) -> bool,
1947            max_depth: usize,
1948        ) -> Option<Vec<&'static str>> {
1949            let descs = self.descs();
1950            let chain = find_chain(&descs, input, target, max_depth)?;
1951            Some(
1952                chain
1953                    .into_iter()
1954                    .map(|link| self.factories[link.index].desc.name)
1955                    .collect(),
1956            )
1957        }
1958
1959        /// Find the shortest chain converting `input` into caps satisfying
1960        /// `target` and instantiate it: an ordered list of boxed elements
1961        /// (upstream first), each configured to produce the caps the search
1962        /// chose for it, ready to splice onto [`run_graph`] as transforms.
1963        /// `Some(vec![])` if no elements are needed; `None` if no chain exists.
1964        ///
1965        /// [`run_graph`]: crate::runtime::run_graph
1966        pub fn autoplug(
1967            &self,
1968            input: &Caps,
1969            target: &dyn Fn(&Caps) -> bool,
1970            max_depth: usize,
1971        ) -> Option<Vec<Box<dyn DynAsyncElement>>> {
1972            let descs = self.descs();
1973            let chain = find_chain(&descs, input, target, max_depth)?;
1974            self.instantiate(chain, &AutoplugParams::new()).ok()
1975        }
1976
1977        /// Build every element of a found chain, each configured to produce the
1978        /// caps the search chose for it, then apply the [`AutoplugParams`]
1979        /// assignments addressed to its factory. With no assignments this cannot
1980        /// fail, which is why the [`Option`]-returning entry points can drop the
1981        /// error.
1982        fn instantiate(
1983            &self,
1984            chain: Vec<ChainLink>,
1985            params: &AutoplugParams,
1986        ) -> Result<Vec<Box<dyn DynAsyncElement>>, AutoplugError> {
1987            chain
1988                .into_iter()
1989                .map(|link| {
1990                    let factory = &self.factories[link.index];
1991                    let mut element = factory.build(&link.output);
1992                    params.apply(factory.desc.name, element.as_mut())?;
1993                    Ok(element)
1994                })
1995                .collect()
1996        }
1997
1998        /// `decodebin`-equivalent: auto-plug a decode chain and splice it into
1999        /// `graph` as a run of transforms between an existing output pad `from`
2000        /// (which produces `input` caps) and an existing input pad `to`. Returns
2001        /// the inserted transform node ids in chain order.
2002        ///
2003        /// This is the "returns a sub-graph onto `run_graph`" payoff: the caller
2004        /// builds its source and sink, names the input caps and the target shape
2005        /// ([`is_raw_video`] for playback), and the registry fills the middle. An
2006        /// empty chain (input already satisfies `target`) links `from` straight
2007        /// to `to`.
2008        ///
2009        /// The memory-domain preference comes from the graph itself (M989): the
2010        /// element behind `to` declares what memory it accepts
2011        /// ([`input_domains`](crate::AsyncElement::input_domains)), so a
2012        /// Cuda-only consumer gets the `Cuda`-producing decoder without the
2013        /// caller naming a domain (see
2014        /// [`derived_memory_preference`](Self::derived_memory_preference)).
2015        /// [`decodebin_preferring`](Self::decodebin_preferring) overrides it.
2016        pub fn decodebin(
2017            &self,
2018            graph: &mut Graph<GraphNode>,
2019            from: impl Into<PadId>,
2020            to: impl Into<PadId>,
2021            input: &Caps,
2022            target: &dyn Fn(&Caps) -> bool,
2023            max_depth: usize,
2024        ) -> Result<Vec<NodeId>, DecodebinError> {
2025            let to: PadId = to.into();
2026            let preferred = Self::derived_memory_preference(graph, to);
2027            let mut elements = self
2028                .autoplug_preferring(input, target, max_depth, preferred)
2029                .ok_or(DecodebinError::NoChain)?;
2030            let _ = self.maybe_prepend_parser(input, &mut elements);
2031            Self::splice_chain(graph, from, to, elements)
2032        }
2033
2034        /// The memory domain the decode chain's consumer wants, read off the
2035        /// element behind the `to` pad: the most-preferred domain of its declared
2036        /// [`input_domains`](crate::AsyncElement::input_domains) (GPU-resident
2037        /// before `System`, per `DomainSet`). An element that declares no
2038        /// requirement ([`DomainSet::ALL`], the default) or a pad with no element
2039        /// behind it (a tee) derives `System`, the plain selection, so an ordinary
2040        /// graph is unaffected.
2041        ///
2042        /// Only the immediate consumer is consulted, never a chain of them: a
2043        /// default `ALL` means "declares no requirement", not "passes any domain
2044        /// through", and most CPU elements never declare, so walking past them
2045        /// would hand a GPU frame to an element that can only read host bytes.
2046        pub fn derived_memory_preference(graph: &Graph<GraphNode>, to: PadId) -> MemoryDomainKind {
2047            let accepted = graph
2048                .element(to.node)
2049                .map(|node| node.input_domains())
2050                .unwrap_or(DomainSet::ALL);
2051            memory_preference(accepted)
2052        }
2053
2054        /// The memory domain the element registered under `name` wants its frames
2055        /// in, the same rule as
2056        /// [`derived_memory_preference`](Self::derived_memory_preference) but
2057        /// reached by launch name rather than through a built graph (M1018): the
2058        /// text parser expands a `decodebin` before any element exists, so its
2059        /// consumer is still just a name. An unregistered name derives `System`.
2060        ///
2061        /// The name is default-constructed to be asked, since `input_domains` is a
2062        /// per-instance method and every implementation of it is constant per
2063        /// element type; a factory-level copy of the same fact could drift from it.
2064        /// A launch factory's constructor takes no arguments and opens nothing (a
2065        /// sink reaches its device in `configure_pipeline`), so the throwaway
2066        /// instance costs an allocation.
2067        pub fn declared_memory_preference(&self, name: &str) -> MemoryDomainKind {
2068            self.make_element(name)
2069                .map(|element| memory_preference(element.input_domains()))
2070                .unwrap_or(MemoryDomainKind::System)
2071        }
2072
2073        /// [`decodebin`](Self::decodebin) with per-element property assignments
2074        /// (see [`AutoplugParams`]): each element the search selects, plus the
2075        /// injected parser, gets the assignments addressed to its factory name
2076        /// applied before it is spliced into the graph. The consumer's memory
2077        /// domain is derived the same way as in [`decodebin`](Self::decodebin).
2078        #[allow(clippy::too_many_arguments)]
2079        pub fn decodebin_with_params(
2080            &self,
2081            graph: &mut Graph<GraphNode>,
2082            from: impl Into<PadId>,
2083            to: impl Into<PadId>,
2084            input: &Caps,
2085            target: &dyn Fn(&Caps) -> bool,
2086            max_depth: usize,
2087            params: &AutoplugParams,
2088        ) -> Result<Vec<NodeId>, AutoplugError> {
2089            let to: PadId = to.into();
2090            let mut elements = self.autoplug_with_params(
2091                input,
2092                target,
2093                max_depth,
2094                SelectionContext {
2095                    preferred_memory: Self::derived_memory_preference(graph, to),
2096                    prefer_hardware: false,
2097                },
2098                params,
2099            )?;
2100            if let Some(parser) = self.maybe_prepend_parser(input, &mut elements) {
2101                params.apply(parser, elements[0].as_mut())?;
2102            }
2103            Ok(Self::splice_chain(graph, from, to, elements)?)
2104        }
2105
2106        /// Insert `elements` as a run of transforms between output pad `from` and
2107        /// input pad `to`, returning the inserted node ids in chain order. The
2108        /// shared splice for [`decodebin`](Self::decodebin) and
2109        /// [`decodebin_preferring`](Self::decodebin_preferring).
2110        fn splice_chain(
2111            graph: &mut Graph<GraphNode>,
2112            from: impl Into<PadId>,
2113            to: impl Into<PadId>,
2114            elements: Vec<Box<dyn DynAsyncElement>>,
2115        ) -> Result<Vec<NodeId>, DecodebinError> {
2116            let mut prev: PadId = from.into();
2117            let to: PadId = to.into();
2118            let mut inserted = Vec::with_capacity(elements.len());
2119            for boxed in elements {
2120                let node = graph.add_transform(GraphNodeRef::Element(boxed));
2121                graph.link(prev, node)?;
2122                inserted.push(node);
2123                prev = node.into();
2124            }
2125            graph.link(prev, to)?;
2126            Ok(inserted)
2127        }
2128
2129        /// Domain-aware [`autoplug_names`](Self::autoplug_names): bias ties toward a
2130        /// chain whose terminal element emits the `preferred` memory feature (see
2131        /// [`ElementDesc::output_memory`]). `MemoryDomainKind::System` reproduces
2132        /// the default selection; `Cuda` prefers e.g. `NvDec` over a CPU decoder.
2133        pub fn autoplug_names_preferring(
2134            &self,
2135            input: &Caps,
2136            target: &dyn Fn(&Caps) -> bool,
2137            max_depth: usize,
2138            preferred: MemoryDomainKind,
2139        ) -> Option<Vec<&'static str>> {
2140            let descs = self.descs();
2141            let chain = find_chain_preferring(&descs, input, target, max_depth, preferred)?;
2142            Some(
2143                chain
2144                    .into_iter()
2145                    .map(|link| self.factories[link.index].desc.name)
2146                    .collect(),
2147            )
2148        }
2149
2150        /// Domain-aware [`autoplug`](Self::autoplug): instantiate the chain the
2151        /// domain-aware search picks (see
2152        /// [`autoplug_names_preferring`](Self::autoplug_names_preferring)).
2153        pub fn autoplug_preferring(
2154            &self,
2155            input: &Caps,
2156            target: &dyn Fn(&Caps) -> bool,
2157            max_depth: usize,
2158            preferred: MemoryDomainKind,
2159        ) -> Option<Vec<Box<dyn DynAsyncElement>>> {
2160            let descs = self.descs();
2161            let chain = find_chain_preferring(&descs, input, target, max_depth, preferred)?;
2162            self.instantiate(chain, &AutoplugParams::new()).ok()
2163        }
2164
2165        /// Capability-aware [`autoplug_names`](Self::autoplug_names): score
2166        /// candidates against `ctx` ([`SelectionContext`]) to choose among
2167        /// elements satisfying the same caps (memory domain, hardware, then a rank
2168        /// tiebreaker). The generalization of
2169        /// [`autoplug_names_preferring`](Self::autoplug_names_preferring) (which is
2170        /// the memory-only case); a default `ctx` is the plain selection.
2171        pub fn autoplug_names_with(
2172            &self,
2173            input: &Caps,
2174            target: &dyn Fn(&Caps) -> bool,
2175            max_depth: usize,
2176            ctx: SelectionContext,
2177        ) -> Option<Vec<&'static str>> {
2178            let descs = self.descs();
2179            let chain = find_chain_with(&descs, input, target, max_depth, ctx)?;
2180            Some(
2181                chain
2182                    .into_iter()
2183                    .map(|link| self.factories[link.index].desc.name)
2184                    .collect(),
2185            )
2186        }
2187
2188        /// Capability-aware [`autoplug`](Self::autoplug): instantiate the chain the
2189        /// capability-scored search ([`autoplug_names_with`](Self::autoplug_names_with))
2190        /// picks.
2191        pub fn autoplug_with(
2192            &self,
2193            input: &Caps,
2194            target: &dyn Fn(&Caps) -> bool,
2195            max_depth: usize,
2196            ctx: SelectionContext,
2197        ) -> Option<Vec<Box<dyn DynAsyncElement>>> {
2198            let descs = self.descs();
2199            let chain = find_chain_with(&descs, input, target, max_depth, ctx)?;
2200            self.instantiate(chain, &AutoplugParams::new()).ok()
2201        }
2202
2203        /// Capability-aware [`autoplug`](Self::autoplug) that also applies
2204        /// `params` to each element it builds (see [`AutoplugParams`]). The
2205        /// instantiation half of [`decodebin_with_params`](Self::decodebin_with_params).
2206        pub fn autoplug_with_params(
2207            &self,
2208            input: &Caps,
2209            target: &dyn Fn(&Caps) -> bool,
2210            max_depth: usize,
2211            ctx: SelectionContext,
2212            params: &AutoplugParams,
2213        ) -> Result<Vec<Box<dyn DynAsyncElement>>, AutoplugError> {
2214            let descs = self.descs();
2215            let chain = find_chain_with(&descs, input, target, max_depth, ctx)
2216                .ok_or(AutoplugError::NoChain)?;
2217            self.instantiate(chain, params)
2218        }
2219
2220        /// Domain-aware [`decodebin`](Self::decodebin): splice in the chain the
2221        /// domain-aware search picks, biased toward `preferred` memory (e.g. `Cuda`
2222        /// to prefer `NvDec` when the downstream consumer is GPU-resident).
2223        ///
2224        /// `preferred` wins over what `decodebin` would derive from the consumer's
2225        /// declared input domains, so this is the caller's override (ask for
2226        /// `System` and a Cuda-accepting consumer still gets the CPU decoder, with
2227        /// the converter auto-plug uploading on the edge).
2228        #[allow(clippy::too_many_arguments)]
2229        pub fn decodebin_preferring(
2230            &self,
2231            graph: &mut Graph<GraphNode>,
2232            from: impl Into<PadId>,
2233            to: impl Into<PadId>,
2234            input: &Caps,
2235            target: &dyn Fn(&Caps) -> bool,
2236            max_depth: usize,
2237            preferred: MemoryDomainKind,
2238        ) -> Result<Vec<NodeId>, DecodebinError> {
2239            let mut elements = self
2240                .autoplug_preferring(input, target, max_depth, preferred)
2241                .ok_or(DecodebinError::NoChain)?;
2242            let _ = self.maybe_prepend_parser(input, &mut elements);
2243            Self::splice_chain(graph, from, to, elements)
2244        }
2245
2246        /// `playbin`-equivalent: assemble a complete runnable graph from a
2247        /// registered source name and a sink, auto-plugging the decode chain in
2248        /// between. Looks up the source factory, takes its declared output caps
2249        /// as the `decodebin` input, and returns `source -> chain -> sink` ready
2250        /// for [`run_graph`](crate::runtime::run_graph). This is the "just play
2251        /// this" entry point, minus the URI-scheme front door (the caller still
2252        /// names the source rather than passing a `uri=`).
2253        pub fn build_playbin<Sk: AsyncElement + 'static>(
2254            &self,
2255            source_name: &str,
2256            sink: Sk,
2257            target: &dyn Fn(&Caps) -> bool,
2258            max_depth: usize,
2259        ) -> Result<Graph<GraphNode>, PlaybinError> {
2260            let source = self
2261                .sources
2262                .iter()
2263                .find(|s| s.name == source_name)
2264                .ok_or(PlaybinError::UnknownSource)?;
2265            let mut graph: Graph<GraphNode> = Graph::new();
2266            let src = graph.add_source(GraphNodeRef::Source((source.build)()));
2267            let snk = graph.add_sink(GraphNodeRef::element(sink));
2268            self.decodebin(&mut graph, src, snk, &source.output, target, max_depth)?;
2269            Ok(graph)
2270        }
2271
2272        /// [`build_playbin`](Self::build_playbin) with per-element property
2273        /// assignments for the auto-plugged chain (see [`AutoplugParams`]).
2274        pub fn build_playbin_with_params<Sk: AsyncElement + 'static>(
2275            &self,
2276            source_name: &str,
2277            sink: Sk,
2278            target: &dyn Fn(&Caps) -> bool,
2279            max_depth: usize,
2280            params: &AutoplugParams,
2281        ) -> Result<Graph<GraphNode>, AutoplugError> {
2282            let source = self
2283                .sources
2284                .iter()
2285                .find(|s| s.name == source_name)
2286                .ok_or(AutoplugError::UnknownSource)?;
2287            let mut graph: Graph<GraphNode> = Graph::new();
2288            let src = graph.add_source(GraphNodeRef::Source((source.build)()));
2289            let snk = graph.add_sink(GraphNodeRef::element(sink));
2290            self.decodebin_with_params(
2291                &mut graph,
2292                src,
2293                snk,
2294                &source.output,
2295                target,
2296                max_depth,
2297                params,
2298            )?;
2299            Ok(graph)
2300        }
2301
2302        /// `uridecodebin`-equivalent: the URI-scheme front door to
2303        /// [`build_playbin`](Self::build_playbin). Parses `uri`, dispatches to
2304        /// the registered [`UriSourceFactory`] for its scheme to construct the
2305        /// source from the URI, then auto-plugs `source -> chain -> sink` down
2306        /// to `target`, returning a graph ready for
2307        /// [`run_graph`](crate::runtime::run_graph).
2308        ///
2309        /// `target` is a shape predicate (commonly [`is_raw_video`] for
2310        /// playback); the source's runtime caps are resolved at negotiation, so
2311        /// the handler's declared output caps only need to name the *media type*
2312        /// the right decoder is plugged for.
2313        /// Dispatch a URI to its registered scheme handler, constructing the
2314        /// source from the URI and returning it with the caps it produces (the
2315        /// decode-chain input). The lower half of
2316        /// [`build_uridecodebin`](Self::build_uridecodebin), exposed so the
2317        /// `uridecodebin` / `playbin` text-parser nodes can splice the source into
2318        /// a larger graph instead of getting a complete one.
2319        pub fn build_uri_source(
2320            &self,
2321            uri: &str,
2322        ) -> Result<(Box<dyn DynSourceLoop>, Caps), UriError> {
2323            let parsed = Uri::parse(uri).ok_or(UriError::Malformed)?;
2324            let handler = self
2325                .uris
2326                .iter()
2327                .find(|h| h.scheme == parsed.scheme)
2328                .ok_or(UriError::UnknownScheme)?;
2329            (handler.build)(&parsed)
2330        }
2331
2332        pub fn build_uridecodebin<Sk: AsyncElement + 'static>(
2333            &self,
2334            uri: &str,
2335            sink: Sk,
2336            target: &dyn Fn(&Caps) -> bool,
2337            max_depth: usize,
2338        ) -> Result<Graph<GraphNode>, UriError> {
2339            let (source, output) = self.build_uri_source(uri)?;
2340            self.build_source_decodebin(source, &output, sink, target, max_depth)
2341        }
2342
2343        /// Auto-plug `source -> chain -> sink` from an *already constructed*
2344        /// source whose output is `source_caps`, the lower half of
2345        /// [`build_uridecodebin`](Self::build_uridecodebin) factored out so a
2346        /// caller that built (or wrapped) its own source can still get the decode
2347        /// chain auto-plugged. A gapless playlist source (`GaplessSrc`) uses this
2348        /// to splice its decode chain without the URI-handler step.
2349        pub fn build_source_decodebin<Sk: AsyncElement + 'static>(
2350            &self,
2351            source: Box<dyn DynSourceLoop>,
2352            source_caps: &Caps,
2353            sink: Sk,
2354            target: &dyn Fn(&Caps) -> bool,
2355            max_depth: usize,
2356        ) -> Result<Graph<GraphNode>, UriError> {
2357            let mut graph: Graph<GraphNode> = Graph::new();
2358            let src = graph.add_source(GraphNodeRef::Source(source));
2359            let snk = graph.add_sink(GraphNodeRef::element(sink));
2360            self.decodebin(&mut graph, src, snk, source_caps, target, max_depth)?;
2361            Ok(graph)
2362        }
2363
2364        /// `playbin`-equivalent (M379): assemble a complete runnable graph that
2365        /// splits a container into its selected streams and decodes each to its own
2366        /// sink. Builds the source from `uri`, adds `demux` (a
2367        /// [`MultiOutputElement`], e.g. `MkvDemuxN`) as a fan-out node, and for each
2368        /// [`PlaybinPort`] (one per selected stream, in port order) auto-plugs a
2369        /// decode chain from that port's elementary caps to its sink. Returns
2370        /// `source -> demux -> {decode chain -> sink}` ready for
2371        /// [`run_graph`](crate::runtime::run_graph), the multi-stream counterpart of
2372        /// [`build_uridecodebin`](Self::build_uridecodebin).
2373        ///
2374        /// The app derives `ports` from the demux's announced
2375        /// [`StreamCollection`](crate::stream::StreamCollection) (M376) and its
2376        /// selection (M377); `demux`'s port count must equal `ports.len()`. Each
2377        /// branch retypes from the demux's (byte-stream) input caps to its
2378        /// elementary stream via the per-port `CapsChanged` the demux emits, so a
2379        /// branch element must tolerate the startup broadcast and re-solve then (the
2380        /// M210 demux-node contract); per-branch static negotiation against the port
2381        /// caps is a follow-up.
2382        pub fn build_playbin_graph<D: MultiOutputElement + 'static>(
2383            &self,
2384            uri: &str,
2385            demux: D,
2386            ports: Vec<PlaybinPort>,
2387            max_depth: usize,
2388        ) -> Result<Graph<GraphNode>, PlaybinGraphError> {
2389            if ports.is_empty() {
2390                return Err(PlaybinGraphError::NoPorts);
2391            }
2392            let (source, _byte_caps) = self.build_uri_source(uri)?;
2393            self.build_playbin_graph_with_source(source, demux, ports, max_depth)
2394        }
2395
2396        /// [`build_playbin_graph`](Self::build_playbin_graph) with per-element
2397        /// property assignments applied to every branch's auto-plugged chain (see
2398        /// [`AutoplugParams`]). The same assignments are offered to each branch, so
2399        /// a factory selected on two branches is configured identically.
2400        pub fn build_playbin_graph_with_params<D: MultiOutputElement + 'static>(
2401            &self,
2402            uri: &str,
2403            demux: D,
2404            ports: Vec<PlaybinPort>,
2405            max_depth: usize,
2406            params: &AutoplugParams,
2407        ) -> Result<Graph<GraphNode>, AutoplugError> {
2408            if ports.is_empty() {
2409                return Err(AutoplugError::NoPorts);
2410            }
2411            let (source, _byte_caps) = self.build_uri_source(uri)?;
2412            let mut graph: Graph<GraphNode> = Graph::new();
2413            let src = graph.add_source(GraphNodeRef::Source(source));
2414            let outputs = ports.len() as u8;
2415            let demux = graph.add_demux(GraphNode::demux(demux), outputs);
2416            graph.link(src, demux.input())?;
2417            for (i, port) in ports.into_iter().enumerate() {
2418                let snk = graph.add_sink(GraphNodeRef::Element(port.sink));
2419                self.decodebin_with_params(
2420                    &mut graph,
2421                    demux.out(i as u8),
2422                    snk,
2423                    &port.input_caps,
2424                    &*port.target,
2425                    max_depth,
2426                    params,
2427                )?;
2428            }
2429            Ok(graph)
2430        }
2431
2432        /// Like [`build_playbin_graph`](Self::build_playbin_graph) but with a
2433        /// pre-built byte source instead of one derived from the URI's scheme
2434        /// handler. The `playbin uri=` auto-fan-out hook (M382) uses this: having
2435        /// probed the file to choose the demuxer, it already knows the container,
2436        /// so it supplies the matching raw-byte source directly rather than the
2437        /// URI handler's source (which, for `file://`, may self-demux a *different*
2438        /// container, e.g. MP4). `source` must emit the byte stream `demux`
2439        /// expects.
2440        pub fn build_playbin_graph_with_source<D: MultiOutputElement + 'static>(
2441            &self,
2442            source: Box<dyn DynSourceLoop>,
2443            demux: D,
2444            ports: Vec<PlaybinPort>,
2445            max_depth: usize,
2446        ) -> Result<Graph<GraphNode>, PlaybinGraphError> {
2447            if ports.is_empty() {
2448                return Err(PlaybinGraphError::NoPorts);
2449            }
2450            let mut graph: Graph<GraphNode> = Graph::new();
2451            let src = graph.add_source(GraphNodeRef::Source(source));
2452            let outputs = ports.len() as u8;
2453            let demux = graph.add_demux(GraphNode::demux(demux), outputs);
2454            graph.link(src, demux.input())?;
2455            for (i, port) in ports.into_iter().enumerate() {
2456                let snk = graph.add_sink(GraphNodeRef::Element(port.sink));
2457                self.decodebin(
2458                    &mut graph,
2459                    demux.out(i as u8),
2460                    snk,
2461                    &port.input_caps,
2462                    &*port.target,
2463                    max_depth,
2464                )?;
2465            }
2466            Ok(graph)
2467        }
2468    }
2469}
2470
2471#[cfg(feature = "std")]
2472pub use factory::{
2473    declared_source_caps, AutoplugError, AutoplugParams, DecodebinError, DecodebinSelectHook,
2474    DemuxFactory, DemuxSelectHook, ElementDoc, ElementFactory, FanoutSrcFactory, LaunchFactory,
2475    MuxerFactory, PlaybinError, PlaybinGraphError, PlaybinHook, PlaybinPort, PrimaryStream,
2476    PrimaryStreamHook, PropertyDoc, Registry, SourceFactory, Uri, UriError, UriSourceFactory,
2477};
2478
2479#[cfg(test)]
2480mod tests {
2481    use super::*;
2482    use crate::caps::{Dim, Rate, RawVideoFormat, VideoCodec};
2483
2484    fn h264(width: Dim) -> Caps {
2485        Caps::CompressedVideo {
2486            codec: VideoCodec::H264,
2487            width,
2488            height: Dim::Any,
2489            framerate: Rate::Any,
2490        }
2491    }
2492
2493    fn raw(format: RawVideoFormat) -> Caps {
2494        Caps::RawVideo {
2495            format,
2496            width: Dim::Any,
2497            height: Dim::Any,
2498            framerate: Rate::Any,
2499            interlace: crate::Interlace::Any,
2500        }
2501    }
2502
2503    /// H.264 in, H.264 out (a parser: refines but never changes media type).
2504    fn parser() -> ElementDesc {
2505        ElementDesc::new(
2506            "h264parse",
2507            Vec::from([
2508                PadTemplate::sink(CapsSet::one(h264(Dim::Any))),
2509                PadTemplate::source(CapsSet::one(h264(Dim::Any))),
2510            ]),
2511        )
2512    }
2513
2514    /// H.264 in, raw NV12 out (a decoder).
2515    fn decoder() -> ElementDesc {
2516        ElementDesc::new(
2517            "h264dec",
2518            Vec::from([
2519                PadTemplate::sink(CapsSet::one(h264(Dim::Any))),
2520                PadTemplate::source(CapsSet::one(raw(RawVideoFormat::Nv12))),
2521            ]),
2522        )
2523    }
2524
2525    /// Raw NV12 in, raw RGBA out (a converter).
2526    fn convert() -> ElementDesc {
2527        ElementDesc::new(
2528            "videoconvert",
2529            Vec::from([
2530                PadTemplate::sink(CapsSet::one(raw(RawVideoFormat::Nv12))),
2531                PadTemplate::source(CapsSet::one(raw(RawVideoFormat::Rgba8))),
2532            ]),
2533        )
2534    }
2535
2536    /// A second H.264 -> NV12 decoder tagged as a Cuda-producing hardware path
2537    /// (a native NVDEC analog), to exercise capability-based selection.
2538    fn gpu_decoder() -> ElementDesc {
2539        ElementDesc::new(
2540            "nvdec",
2541            Vec::from([
2542                PadTemplate::sink(CapsSet::one(h264(Dim::Any))),
2543                PadTemplate::source(CapsSet::one(raw(RawVideoFormat::Nv12))),
2544            ]),
2545        )
2546        .with_output_memory(MemoryDomainKind::Cuda)
2547        .with_acceleration(Acceleration::Hardware)
2548    }
2549
2550    /// Just the descriptor indices of a found chain, for terse assertions.
2551    fn indices(chain: &[ChainLink]) -> Vec<usize> {
2552        chain.iter().map(|l| l.index).collect()
2553    }
2554
2555    #[test]
2556    fn capability_score_ranks_domain_then_hardware_then_rank() {
2557        let sys = CapabilityDescriptor::default();
2558        let cuda = CapabilityDescriptor {
2559            output_memory: MemoryDomainKind::Cuda,
2560            ..Default::default()
2561        };
2562        let ctx_cuda = SelectionContext {
2563            preferred_memory: MemoryDomainKind::Cuda,
2564            prefer_hardware: false,
2565        };
2566        assert!(
2567            cuda.score(&ctx_cuda) > sys.score(&ctx_cuda),
2568            "a memory-domain match dominates"
2569        );
2570
2571        let hw = CapabilityDescriptor {
2572            acceleration: Acceleration::Hardware,
2573            ..Default::default()
2574        };
2575        let ctx_hw = SelectionContext {
2576            preferred_memory: MemoryDomainKind::System,
2577            prefer_hardware: true,
2578        };
2579        assert!(
2580            hw.score(&ctx_hw) > sys.score(&ctx_hw),
2581            "a hardware preference favors hardware"
2582        );
2583
2584        let ranked = CapabilityDescriptor {
2585            rank: 5,
2586            ..Default::default()
2587        };
2588        let plain = SelectionContext::default();
2589        assert!(
2590            ranked.score(&plain) > sys.score(&plain),
2591            "rank breaks an otherwise-equal tie"
2592        );
2593    }
2594
2595    #[test]
2596    fn default_context_keeps_registration_order() {
2597        // CPU decoder first, GPU second: a plain search picks the CPU decoder, so
2598        // a default pipeline's selection is unchanged by the capability machinery.
2599        let descs = [decoder(), gpu_decoder()];
2600        let chain = find_chain_with(
2601            &descs,
2602            &h264(Dim::Any),
2603            &is_raw_video,
2604            4,
2605            SelectionContext::default(),
2606        )
2607        .expect("a decoder reaches raw");
2608        assert_eq!(
2609            indices(&chain),
2610            Vec::from([0usize]),
2611            "default = registration order (CPU first)"
2612        );
2613    }
2614
2615    #[test]
2616    fn cuda_preference_selects_the_gpu_decoder() {
2617        // The same two decoders; a Cuda preference flips the choice to the GPU
2618        // decoder even though it is registered second (avoids a download).
2619        let descs = [decoder(), gpu_decoder()];
2620        let ctx = SelectionContext {
2621            preferred_memory: MemoryDomainKind::Cuda,
2622            prefer_hardware: false,
2623        };
2624        let chain =
2625            find_chain_with(&descs, &h264(Dim::Any), &is_raw_video, 4, ctx).expect("reaches raw");
2626        assert_eq!(
2627            indices(&chain),
2628            Vec::from([1usize]),
2629            "Cuda preference picks the GPU decoder"
2630        );
2631    }
2632
2633    #[test]
2634    fn rank_breaks_ties_among_equal_candidates() {
2635        // Two equivalent CPU decoders; the higher-ranked one wins regardless of
2636        // registration order (the explicit-override tiebreaker).
2637        let descs = [decoder(), decoder().with_rank(10)];
2638        let chain = find_chain_with(
2639            &descs,
2640            &h264(Dim::Any),
2641            &is_raw_video,
2642            4,
2643            SelectionContext::default(),
2644        )
2645        .expect("reaches raw");
2646        assert_eq!(
2647            indices(&chain),
2648            Vec::from([1usize]),
2649            "higher rank wins the tie"
2650        );
2651    }
2652
2653    #[test]
2654    fn finds_single_decoder_for_h264_to_raw() {
2655        let descs = [parser(), decoder()];
2656        let chain = find_chain(&descs, &h264(Dim::Fixed(1280)), &is_raw_video, 4)
2657            .expect("decoder bridges H.264 to raw");
2658        // Shortest path is the decoder alone (the parser is same-shape, so it
2659        // never shortens the route to raw), and it was chosen to emit NV12.
2660        assert_eq!(indices(&chain), Vec::from([1usize]));
2661        assert_eq!(chain[0].output, raw(RawVideoFormat::Nv12));
2662    }
2663
2664    #[test]
2665    fn empty_chain_when_input_already_satisfies_target() {
2666        let descs = [decoder()];
2667        let chain =
2668            find_chain(&descs, &raw(RawVideoFormat::Nv12), &is_raw_video, 4).expect("already raw");
2669        assert!(
2670            chain.is_empty(),
2671            "no elements needed when input is already raw"
2672        );
2673    }
2674
2675    #[test]
2676    fn finds_multi_element_chain_to_a_specific_format() {
2677        // Target a format only the converter produces, forcing decoder -> convert.
2678        let descs = [parser(), decoder(), convert()];
2679        let target = |c: &Caps| {
2680            matches!(
2681                c,
2682                Caps::RawVideo {
2683                    format: RawVideoFormat::Rgba8,
2684                    ..
2685                }
2686            )
2687        };
2688        let chain = find_chain(&descs, &h264(Dim::Any), &target, 4)
2689            .expect("decode then convert reaches RGBA");
2690        assert_eq!(
2691            indices(&chain),
2692            Vec::from([1usize, 2usize]),
2693            "decoder then converter"
2694        );
2695        // The converter hop carries the chosen output the builder needs: RGBA.
2696        assert_eq!(chain.last().unwrap().output, raw(RawVideoFormat::Rgba8));
2697    }
2698
2699    #[test]
2700    fn no_chain_when_target_unreachable() {
2701        // Only a parser is registered: H.264 can never become raw.
2702        let descs = [parser()];
2703        assert!(
2704            find_chain(&descs, &h264(Dim::Any), &is_raw_video, 8).is_none(),
2705            "a parser alone cannot reach raw video"
2706        );
2707    }
2708
2709    #[test]
2710    fn respects_max_depth() {
2711        // The decoder -> convert chain is length 2; a depth bound of 1 can't
2712        // reach the RGBA-only target.
2713        let descs = [decoder(), convert()];
2714        let target = |c: &Caps| {
2715            matches!(
2716                c,
2717                Caps::RawVideo {
2718                    format: RawVideoFormat::Rgba8,
2719                    ..
2720                }
2721            )
2722        };
2723        assert!(
2724            find_chain(&descs, &h264(Dim::Any), &target, 1).is_none(),
2725            "1 hop is too shallow"
2726        );
2727        assert!(
2728            find_chain(&descs, &h264(Dim::Any), &target, 2).is_some(),
2729            "2 hops suffice"
2730        );
2731    }
2732
2733    #[cfg(feature = "std")]
2734    #[test]
2735    fn uri_parse_splits_scheme_and_rest() {
2736        let u = Uri::parse("rtsp://cam.local:554/stream1?tcp").expect("valid uri");
2737        assert_eq!(u.scheme, "rtsp");
2738        assert_eq!(u.rest, "cam.local:554/stream1?tcp");
2739        assert_eq!(u.raw, "rtsp://cam.local:554/stream1?tcp");
2740
2741        let f = Uri::parse("file:///home/a/clip.mp4").expect("valid file uri");
2742        assert_eq!(f.scheme, "file");
2743        assert_eq!(
2744            f.rest, "/home/a/clip.mp4",
2745            "file:// leaves an absolute path"
2746        );
2747
2748        let udp = Uri::parse("udp://0.0.0.0:5004").expect("valid udp uri");
2749        assert_eq!((udp.scheme, udp.rest), ("udp", "0.0.0.0:5004"));
2750    }
2751
2752    #[cfg(feature = "std")]
2753    #[test]
2754    fn uri_parse_rejects_malformed() {
2755        assert!(Uri::parse("notauri").is_none(), "no scheme separator");
2756        assert!(Uri::parse("://nohost").is_none(), "empty scheme");
2757    }
2758
2759    /// A trivial element for graph-shape tests: accepts any caps, never runs. The
2760    /// registered factory's pad-template descriptor (not this body) drives autoplug.
2761    #[cfg(feature = "std")]
2762    #[derive(Debug, Default)]
2763    struct Dummy;
2764    #[cfg(feature = "std")]
2765    impl crate::PadTemplates for Dummy {
2766        fn pad_templates() -> Vec<PadTemplate> {
2767            Vec::new()
2768        }
2769    }
2770    #[cfg(feature = "std")]
2771    impl crate::AsyncElement for Dummy {
2772        type ProcessFuture<'a> = core::pin::Pin<
2773            alloc::boxed::Box<dyn core::future::Future<Output = Result<(), crate::G2gError>> + 'a>,
2774        >;
2775        fn intercept_caps(&self, c: &Caps) -> Result<Caps, crate::G2gError> {
2776            Ok(c.clone())
2777        }
2778        fn configure_pipeline(
2779            &mut self,
2780            _c: &Caps,
2781        ) -> Result<crate::ConfigureOutcome, crate::G2gError> {
2782            Ok(crate::ConfigureOutcome::Accepted)
2783        }
2784        fn process<'a>(
2785            &'a mut self,
2786            _p: crate::PipelinePacket,
2787            _o: &'a mut dyn crate::OutputSink,
2788        ) -> Self::ProcessFuture<'a> {
2789            alloc::boxed::Box::pin(async { Ok(()) })
2790        }
2791    }
2792
2793    /// M421: a configured `parser_provider` prepends its parser to every auto-plugged
2794    /// decode chain (so a decoder is fed access-unit-aligned input), and only then.
2795    #[cfg(feature = "std")]
2796    #[test]
2797    fn decodebin_inserts_the_parser_provider_before_the_decoder() {
2798        use crate::runtime::{GraphNode, GraphNodeRef};
2799        use crate::Graph;
2800
2801        // An H.264 -> raw "decoder" factory; the element body is irrelevant to the
2802        // graph shape, so a Dummy stands in.
2803        fn dec_factory() -> ElementFactory {
2804            ElementFactory::new(
2805                "h264dec",
2806                Vec::from([
2807                    PadTemplate::sink(CapsSet::one(h264(Dim::Any))),
2808                    PadTemplate::source(CapsSet::one(raw(RawVideoFormat::Nv12))),
2809                ]),
2810                |_| alloc::boxed::Box::new(Dummy),
2811            )
2812        }
2813        let build = |provider: bool| -> usize {
2814            let mut reg = Registry::new();
2815            reg.register(dec_factory());
2816            if provider {
2817                // The provider names a registered launch element (M676), the
2818                // identity-caps re-framing parser.
2819                reg.register_launch(LaunchFactory::new(
2820                    "auparse",
2821                    Vec::from([
2822                        PadTemplate::sink(CapsSet::one(h264(Dim::Any))),
2823                        PadTemplate::source(CapsSet::one(h264(Dim::Any))),
2824                    ]),
2825                    || alloc::boxed::Box::new(Dummy),
2826                ));
2827                reg.set_parser_provider(|caps| match caps {
2828                    Caps::CompressedVideo {
2829                        codec: VideoCodec::H264,
2830                        ..
2831                    } => Some("auparse"),
2832                    _ => None,
2833                });
2834            }
2835            let mut g: Graph<GraphNode> = Graph::new();
2836            let head = g.add_transform(GraphNodeRef::element(Dummy));
2837            let tail = g.add_sink(GraphNodeRef::element(Dummy));
2838            reg.decodebin(&mut g, head, tail, &h264(Dim::Any), &is_raw_video, 4)
2839                .expect("h264 -> raw chain")
2840                .len()
2841        };
2842        assert_eq!(
2843            build(false),
2844            1,
2845            "no provider: the decode chain is just the decoder"
2846        );
2847        assert_eq!(
2848            build(true),
2849            2,
2850            "provider: a parser is spliced in ahead of the decoder"
2851        );
2852    }
2853
2854    #[cfg(feature = "std")]
2855    fn named_sink(name: &'static str) -> LaunchFactory {
2856        LaunchFactory::new(name, Vec::new(), || alloc::boxed::Box::new(Dummy))
2857    }
2858
2859    #[cfg(feature = "std")]
2860    #[test]
2861    fn an_alias_falls_past_a_target_that_cannot_run_here() {
2862        let mut reg = Registry::new();
2863        reg.register_launch(named_sink("displaysink").with_usable(|| false));
2864        reg.register_launch(named_sink("nullsink"));
2865        reg.register_alias("autosink", &["displaysink", "nullsink"]);
2866
2867        assert_eq!(reg.resolve_alias("autosink"), "nullsink");
2868        assert!(
2869            reg.make_element("displaysink").is_some(),
2870            "naming the sink outright still builds it, so it reports its own failure"
2871        );
2872    }
2873
2874    #[cfg(feature = "std")]
2875    #[test]
2876    fn an_alias_takes_the_first_target_that_can_run_here() {
2877        let mut reg = Registry::new();
2878        reg.register_launch(named_sink("displaysink").with_usable(|| true));
2879        reg.register_launch(named_sink("nullsink"));
2880        reg.register_alias("autosink", &["displaysink", "nullsink"]);
2881
2882        assert_eq!(reg.resolve_alias("autosink"), "displaysink");
2883    }
2884
2885    #[cfg(feature = "std")]
2886    #[test]
2887    fn a_target_declaring_no_check_stays_usable() {
2888        let mut reg = Registry::new();
2889        reg.register_launch(named_sink("plainsink"));
2890        reg.register_alias("autosink", &["plainsink"]);
2891
2892        assert_eq!(reg.resolve_alias("autosink"), "plainsink");
2893    }
2894
2895    #[cfg(feature = "std")]
2896    #[test]
2897    fn inspect_prints_experimental_only_when_declared() {
2898        let mut reg = Registry::new();
2899        reg.register_launch(named_sink("gpusink").with_experimental());
2900        reg.register_launch(named_sink("plainsink"));
2901
2902        let gpu = reg.inspect("gpusink").expect("registered");
2903        assert!(
2904            gpu.contains("Stability   experimental"),
2905            "declared experimental: {gpu}"
2906        );
2907        assert!(
2908            reg.element_listing()
2909                .iter()
2910                .any(|l| l.contains("gpusink") && l.contains("[experimental]")),
2911            "listing tags the name"
2912        );
2913        let doc = reg.describe("gpusink").expect("described");
2914        assert!(doc.experimental);
2915
2916        let plain = reg.inspect("plainsink").expect("registered");
2917        assert!(
2918            !plain.contains("experimental"),
2919            "unmarked factory stays quiet: {plain}"
2920        );
2921        assert!(!reg.describe("plainsink").expect("described").experimental);
2922    }
2923}