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}