g2g_core/runtime/launch.rs
1//! `gst-launch`-style text pipeline parser (M106, M117, M118): turn
2//! `"videotestsrc num-buffers=3 ! videoflip method=rotate-180 ! fakesink"` into a
3//! runnable [`Graph`], the front door that makes g2g usable without hand-writing
4//! Rust for every pipeline.
5//!
6//! Built on the M104 property system and the M105 by-name registry: each `!`
7//! separated node is `element-name key=value ...`; the parser constructs the
8//! element by name from the [`Registry`], looks up each property's
9//! [`PropKind`](crate::PropKind) to parse its textual value, and applies it.
10//! Roles follow connectivity: an element with no incoming link is a source, one
11//! with no outgoing link a sink, the rest transforms (so a linear chain is still
12//! source -> transforms -> sink).
13//!
14//! Branching (M118): `tee name=t` fans one output to many. A `tee` is the
15//! structural fan-out node (no element), its output width derived from how many
16//! branches reference it; a branch is a `t.` pad reference that starts a chain (a
17//! head ref, linking *from* the named element) or, right after a `!`, ends one (a
18//! tail ref, linking *into* it). So
19//! `videotestsrc ! tee name=t ! fakesink t. ! fakesink` broadcasts each frame
20//! to two sinks. The caps shorthand (`! video/x-raw,format=nv12,... !`, M117) is
21//! a bare media-type node rewritten to a `capsfilter`.
22//!
23//! Fan-in (M122): an element with several inbound links is a muxer, built from
24//! the registry's [`MuxerFactory`](crate::runtime::MuxerFactory) with that input
25//! count (so its `input_count` matches the node's pads). Each feeding chain ends
26//! with a `m.` tail ref, so
27//! `src1 ! m. src2 ! m. funnel name=m ! fakesink` joins two streams. Feeding
28//! chains come first (a new chain can only begin after a `!` / ref / caps
29//! boundary, so a chain starting with a bare element name would be read as the
30//! previous element's property); the muxer chain is last. A muxer has one output
31//! pad, so it must feed a downstream consumer.
32//!
33//! Value grammar (M840): a value may carry spaces by quoting (`"..."` or
34//! `'...'`, either anywhere in the fragment: `location=/tmp/"my dir"/a.ts`) or by
35//! escaping (`\ `). A `\` escapes a quote, another `\`, a `!`, a `#`, or
36//! whitespace; before anything else it stays literal, so a Windows path
37//! (`C:\videos\a.ts`) survives unescaped. An enum property's value is checked
38//! against its [`PropertySpec::enum_values`](crate::PropertySpec) before the
39//! element sees it, and a [`Flags`](crate::PropKind::Flags) property takes a
40//! `+`-joined set (`protocols=udp+tcp`), so a bad name names the valid ones.
41//! A pad-name suffix on a reference (`t.src_0`) is accepted but ignored (pads are
42//! positional).
43//!
44//! Encoding profiles (M1089): `encodebin profile="<container>:<stream>[:<stream>]"`
45//! is a macro too, expanding into the encoder each stream names plus the muxer
46//! for the container (`encodebin2` and `transcodebin` are gst's other names for
47//! it, the latter being `decodebin ! encodebin`). The container picks the muxer
48//! and a stream picks its encoder; anything the profile does not name
49//! negotiates. A stream part may also pin what it does care about (M1097): a
50//! `width` / `height`, a `framerate`, a sample `rate` or a `channels` each
51//! splice the converter that applies it, and a `bitrate` is set on the encoder.
52//! Any other field is refused. An uncompressed stream
53//! (`audio/x-raw`) inserts no encoder, and a profile with no container at all is
54//! just the encoder. A converter goes in ahead of an encoder that does not take
55//! what the branch produces (`videoconvert`, or `audioconvert` +
56//! `audioresample`), so a profile works off whatever raw form the source has. With several streams the bin's `name=` moves to the muxer,
57//! so a second branch reaches it as `e.` the way any fan-in does, and that
58//! branch gets the encoder for its own kind of input.
59//!
60//! Two `key=value` pairs are launch keywords rather than properties: `name=` is
61//! the instance name (and the handle pad references resolve against), and
62//! `log-category=` (M847) replaces that instance's `G2G_DEBUG` category, leaving
63//! the auto `<type>N` naming alone.
64
65use alloc::boxed::Box;
66use alloc::string::{String, ToString};
67use alloc::vec::Vec;
68
69use crate::caps::{Caps, CapsSet};
70use crate::element::DynAsyncElement;
71use crate::graph::{Graph, GraphError, NodeId, PadId};
72use crate::link::LinkPolicy;
73use crate::memory::MemoryDomainKind;
74use crate::property::{PropError, PropValue, PropertySpec, ValueError};
75use crate::runtime::autoplug::{
76 is_raw_audio, is_raw_video, PadKind, PadRequest, Registry, UriError,
77};
78use crate::runtime::{DynSourceLoop, GraphNode, GraphNodeRef};
79
80/// Why [`parse_launch`] could not build a graph.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum ParseError {
83 /// The pipeline string was empty or all whitespace.
84 Empty,
85 /// A node between `!` separators had no element name.
86 EmptyStage,
87 /// Fewer than two elements: a runnable pipeline needs at least a source and a
88 /// sink.
89 TooFewStages,
90 /// A source-position element names no registered source.
91 UnknownSource(String),
92 /// A transform / sink-position element names no registered element.
93 UnknownElement(String),
94 /// A property token had no `=` (expected `key=value`).
95 MalformedProperty { element: String, token: String },
96 /// The element has no property of that name.
97 UnknownProperty { element: String, key: String },
98 /// The value did not parse for the property's kind, or was rejected.
99 BadValue {
100 element: String,
101 key: String,
102 value: String,
103 },
104 /// The value names no declared choice of an enum / flag property. `value` is
105 /// the offending nick (one entry of a `+`-joined set), or the whole value when
106 /// the set was malformed; `values` is the property's declared list.
107 BadEnumValue {
108 element: String,
109 key: String,
110 value: String,
111 values: &'static str,
112 },
113 /// A `name.` reference names no element declared with that `name=`.
114 UnknownReference(String),
115 /// Two elements share the same `name=` handle.
116 DuplicateName(String),
117 /// More than one link feeds an element's input, but it names no registered
118 /// muxer: fan-in needs a [`MuxerFactory`](crate::runtime::MuxerFactory).
119 NotAMuxer(String),
120 /// A muxer (an element with several inputs) has no outgoing link; its single
121 /// output pad must feed a downstream consumer.
122 MuxerWithoutOutput(String),
123 /// A named input-pad reference (`mux.foo_0`) names a request pad this muxer
124 /// does not define (M481): the element's `input_pad_index` scheme declined it.
125 UnknownInputPad(String),
126 /// Two input-pad references resolve to the same muxer input index (M481), e.g.
127 /// `mux.video_0` named twice, or a named pad colliding with a positional one.
128 DuplicateInputPad(String),
129 /// A `queue` / `queue2` sits anywhere but a 1-in/1-out position. It is not an
130 /// element in g2g (it collapses into the edge's backpressure policy), so it
131 /// cannot be a source, a sink, or a fan-out / fan-in node.
132 QueueRole(String),
133 /// A `decodebin` has no upstream element to take its input caps from (it was
134 /// the first element, or followed a bare `name.` reference). decodebin
135 /// auto-plugs from its predecessor's declared caps, so it needs one.
136 DecodebinNoUpstream,
137 /// `decodebin` found no chain of registered decoders / parsers from its input
138 /// caps to raw video or audio (the input caps are quoted). Either no decoder
139 /// feature is compiled in, or the input is a container that needs a demuxer
140 /// (auto-plugging through fan-out demuxers is not yet supported).
141 NoDecodeChain(String),
142 /// An `encodebin` had no `profile=`, or one that is not
143 /// `container-caps:stream-caps[:stream-caps...]` (the message says which).
144 EncodingProfile(String),
145 /// An `encodebin` profile named a container no registered muxer writes, or a
146 /// stream codec no compiled-in encoder produces (the message quotes it).
147 NoEncoder(String),
148 /// An `encodebin` chain's input is not raw video or audio, or its profile has
149 /// no stream of that kind, so nothing in the profile can encode it.
150 EncodebinInput(String),
151 /// A `uridecodebin` / `playbin` was not at the head of its chain. It provides
152 /// the source, so it must start the pipeline.
153 UriSourceNotAtHead(String),
154 /// A `uridecodebin` / `playbin` had no `uri=` property.
155 MissingUri(String),
156 /// The `uri=` could not be turned into a source (bad URI, or no handler
157 /// registered for its scheme). The message quotes the URI and reason.
158 Uri(String),
159 /// Linking two nodes into the graph failed.
160 Graph(GraphError),
161}
162
163impl From<GraphError> for ParseError {
164 fn from(e: GraphError) -> Self {
165 ParseError::Graph(e)
166 }
167}
168
169impl core::fmt::Display for ParseError {
170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171 match self {
172 ParseError::Empty => f.write_str("empty pipeline"),
173 ParseError::EmptyStage => f.write_str("empty node between '!' separators"),
174 ParseError::TooFewStages => f.write_str("pipeline needs at least a source and a sink"),
175 ParseError::UnknownSource(n) => write!(f, "unknown source element: {n}"),
176 ParseError::UnknownElement(n) => write!(f, "unknown element: {n}"),
177 ParseError::MalformedProperty { element, token } => {
178 write!(
179 f,
180 "{element}: malformed property '{token}' (expected key=value)"
181 )
182 }
183 ParseError::UnknownProperty { element, key } => {
184 write!(f, "{element}: no property named '{key}'")
185 }
186 ParseError::BadValue {
187 element,
188 key,
189 value,
190 } => {
191 write!(f, "{element}: invalid value '{value}' for property '{key}'")?;
192 // Inline caps become a `capsfilter` (key `caps`); a gst dev often
193 // reaches for range / list / feature syntax g2g's launch parser
194 // does not accept, and the bare "invalid value" hides why.
195 if element == "capsfilter" && key == "caps" && value.contains(['[', '{', '(']) {
196 write!(
197 f,
198 " (a launch caps filter takes fixed fields, ranges [min,max], \
199 and lists {{a,b}} with numeric / known values; caps features \
200 like (memory:...) are not supported)"
201 )?;
202 }
203 Ok(())
204 }
205 ParseError::BadEnumValue {
206 element,
207 key,
208 value,
209 values,
210 } => {
211 write!(f, "{element}: invalid value '{value}' for property '{key}'")?;
212 if !values.is_empty() {
213 write!(f, " (valid: {values})")?;
214 }
215 if value.contains('+') {
216 write!(f, " (a flag set joins nicks with '+', e.g. video+audio)")?;
217 }
218 Ok(())
219 }
220 ParseError::UnknownReference(n) => {
221 write!(f, "reference to undeclared element name: {n}")
222 }
223 ParseError::DuplicateName(n) => write!(f, "duplicate element name: {n}"),
224 ParseError::NotAMuxer(n) => {
225 write!(
226 f,
227 "{n}: more than one input links here, but it is not a registered muxer"
228 )
229 }
230 ParseError::MuxerWithoutOutput(n) => {
231 write!(
232 f,
233 "{n}: muxer has no outgoing link; its output must feed a consumer"
234 )
235 }
236 ParseError::UnknownInputPad(n) => {
237 write!(
238 f,
239 "{n}: no such input request pad (this element defines no pad by that name)"
240 )
241 }
242 ParseError::DuplicateInputPad(n) => {
243 write!(f, "{n}: two inputs resolve to the same request pad index")
244 }
245 ParseError::QueueRole(n) => {
246 write!(f, "{n}: a queue must sit between two elements (1-in/1-out); it maps to an edge policy, not a source/sink/branch")
247 }
248 ParseError::DecodebinNoUpstream => {
249 write!(f, "decodebin has no upstream element to decode; it must follow a source or element with declared caps")
250 }
251 ParseError::NoDecodeChain(caps) => {
252 write!(f, "decodebin: no decoder chain from {caps} to raw (no decoder feature compiled in, or a container that needs a demuxer)")
253 }
254 ParseError::EncodingProfile(msg) => write!(
255 f,
256 "encodebin profile: {msg} (expected profile=\"container-caps:stream-caps[:stream-caps]\", e.g. profile=\"video/x-matroska:video/x-vp8:audio/x-opus\")"
257 ),
258 ParseError::NoEncoder(msg) => write!(f, "encodebin: {msg}"),
259 ParseError::EncodebinInput(msg) => write!(f, "encodebin: {msg}"),
260 ParseError::UriSourceNotAtHead(n) => {
261 write!(f, "{n}: provides the source, so it must start the pipeline (be the first element)")
262 }
263 ParseError::MissingUri(n) => write!(f, "{n}: missing required 'uri=' property"),
264 ParseError::Uri(msg) => write!(f, "uri error: {msg}"),
265 ParseError::Graph(e) => write!(f, "graph link error: {e:?}"),
266 }
267 }
268}
269
270/// One parsed element: factory name plus its `key=value` properties (all owned so
271/// errors can name them), and the optional `name=` handle that pad references
272/// resolve against. `name` and `log-category` are special-cased here (launch
273/// keywords, not properties), never applied as properties.
274struct ElementSpec {
275 name: String,
276 props: Vec<(String, String)>,
277 instance: Option<String>,
278 log_category: Option<String>,
279}
280
281/// An item in a chain: an element to build, a `t.` reference to a named element
282/// declared elsewhere (the branching / link-by-name syntax), or a node already
283/// constructed by a macro expansion (`uridecodebin` / `playbin`, M196), spliced
284/// in directly rather than built by name.
285enum Item {
286 Element(ElementSpec),
287 /// A `t.` / `d.video_0` reference to a named element. `pad` is the suffix after
288 /// the dot (`""` for a bare `t.`, `"video_0"` for `d.video_0`), used by the
289 /// explicit-demux fan-out (M476) to select which stream a branch reads; ignored
290 /// for a tee (positional).
291 Ref {
292 name: String,
293 pad: String,
294 },
295 Prebuilt(PrebuiltNode),
296}
297
298/// A node a macro expansion built ahead of the structural pass: a source
299/// constructed from a `uri=` scheme handler, or a decoder the auto-plug search
300/// instantiated. Spliced into the graph as-is (it has no name to build by).
301enum PrebuiltNode {
302 Source(Box<dyn DynSourceLoop>),
303 Element(Box<dyn DynAsyncElement>),
304}
305
306/// A run of items linked left-to-right by `!`. Branches are separate chains
307/// joined through named references.
308type Chain = Vec<Item>;
309
310/// A caps description node (`video/x-raw,format=nv12,...`): a media type whose
311/// `/` precedes any `=` field. A property value's `/` (a path or a fraction)
312/// comes after its `=`, so it is not mistaken for caps.
313fn is_caps_token(tok: &str) -> bool {
314 match (tok.find('/'), tok.find('=')) {
315 (Some(slash), Some(eq)) => slash < eq,
316 (Some(_), None) => true,
317 _ => false,
318 }
319}
320
321/// A pad reference (`t.`, `t.src_0`, `d.video_0`): a name, a `.`, and no `=` / `/`.
322/// Returns the referenced element name and the pad suffix after the first dot
323/// (`""` for a bare `t.`). The suffix drives the explicit-demux fan-out (M476);
324/// a tee ignores it (positional).
325fn split_pad_ref(tok: &str) -> Option<(&str, &str)> {
326 if tok.contains('=') || tok.contains('/') || !tok.contains('.') {
327 return None;
328 }
329 let (name, pad) = tok.split_once('.').unwrap_or((tok, ""));
330 (!name.is_empty()).then_some((name, pad))
331}
332
333/// The referenced element name of a pad reference (the suffix dropped); the
334/// token-boundary test in [`consume_element`].
335fn as_ref_name(tok: &str) -> Option<&str> {
336 split_pad_ref(tok).map(|(name, _)| name)
337}
338
339/// Parse a demux output-pad suffix into a [`PadRequest`] (M476): `"video_0"` ->
340/// `{ Video, 0 }`, `"audio_1"` -> `{ Audio, 1 }`, `"text_0"` / `"subtitle_0"` ->
341/// `{ Text, 0 }`, `"caption"` -> `{ Text, 0 }`, `"src_2"` -> `{ Any, 2 }`. A bare `d.` (empty suffix) or an
342/// unrecognized prefix is `{ Any, ordinal }`, i.e. positional by reference order.
343fn parse_pad_request(pad: &str, ordinal: usize) -> PadRequest {
344 let (prefix, index) = match pad.rsplit_once('_') {
345 Some((p, n)) => (p, n.parse::<usize>().ok()),
346 None => (pad, None),
347 };
348 let kind = match prefix {
349 "video" => PadKind::Video,
350 "audio" => PadKind::Audio,
351 // `caption` is the name a closed-caption fan-in gives its second pad; it
352 // rides the text kind, which is the one every sidecar-cue pad uses.
353 "text" | "subtitle" | "caption" => PadKind::Text,
354 _ => PadKind::Any,
355 };
356 // `src_N` (output) / `sink_N` (input) and unrecognized prefixes select the Nth
357 // stream / pad; a bare `d.` has no index, so it takes the positional ordinal.
358 let index = match kind {
359 PadKind::Any if prefix == "src" || prefix == "sink" || prefix.is_empty() => {
360 index.unwrap_or(ordinal)
361 }
362 PadKind::Any => ordinal,
363 _ => index.unwrap_or(0),
364 };
365 PadRequest { kind, index }
366}
367
368/// Consume an element's `key=value` properties from the token stream, stopping at
369/// a `!`, a caps node, or a pad reference (the next node begins). A bare token
370/// with no `=` is a malformed property (the gst typo case), reported by name.
371fn consume_element<'a, I: Iterator<Item = &'a str>>(
372 name: &str,
373 tokens: &mut core::iter::Peekable<I>,
374 knows_element: &dyn Fn(&str) -> bool,
375) -> Result<ElementSpec, ParseError> {
376 let mut spec = ElementSpec {
377 name: name.to_string(),
378 props: Vec::new(),
379 instance: None,
380 log_category: None,
381 };
382 while let Some(&tok) = tokens.peek() {
383 if tok == "!" || is_caps_token(tok) || as_ref_name(tok).is_some() {
384 break;
385 }
386 // A bare token naming a registered element opens a new top-level chain
387 // rather than being this element's property, the way gst-launch parses
388 // `videotestsrc ! xvimagesink audiotestsrc ! pulsesink`. One that names
389 // nothing registered is the typo it looks like.
390 if !tok.contains('=') && knows_element(tok) {
391 break;
392 }
393 let (key, value) = tok
394 .split_once('=')
395 .ok_or_else(|| ParseError::MalformedProperty {
396 element: name.to_string(),
397 token: tok.to_string(),
398 })?;
399 tokens.next();
400 let value = unquote_value(value);
401 if key == "name" {
402 spec.instance = Some(value);
403 } else if key == "log-category" {
404 // M847: a launch keyword like `name=`, not a property: it renames this
405 // instance's `G2G_DEBUG` filter key rather than configuring it.
406 spec.log_category = Some(value);
407 } else {
408 spec.props.push((key.to_string(), value));
409 }
410 }
411 Ok(spec)
412}
413
414/// The characters a `\` may escape in a launch line: the quote characters, a `\`
415/// itself, the `!` separator, the `#` comment marker, and whitespace. A `\`
416/// before anything else is literal, so an unescaped Windows path
417/// (`location=C:\videos\a.ts`) survives.
418fn is_escapable(c: char) -> bool {
419 matches!(c, '"' | '\'' | '\\' | '!' | '#') || c.is_whitespace()
420}
421
422/// Resolve a token's quoting into the literal property value: drop the quote
423/// characters that open / close a quoted region and resolve `\x` escapes. A value
424/// may open and close a quoted region more than once
425/// (`location=/tmp/"my dir"/a.ts`), so this walks the whole token rather than
426/// stripping one surrounding pair. Both quote characters work; gst-launch quotes
427/// only with `"` (a `'` is literal there), so this accepts a superset.
428fn unquote_value(v: &str) -> String {
429 let mut out = String::with_capacity(v.len());
430 let mut quote: Option<char> = None;
431 let mut chars = v.chars();
432 while let Some(c) = chars.next() {
433 match c {
434 '\\' => match chars.next() {
435 Some(n) if is_escapable(n) => out.push(n),
436 Some(n) => {
437 out.push('\\');
438 out.push(n);
439 }
440 None => out.push('\\'),
441 },
442 '"' | '\'' if quote.is_none() => quote = Some(c),
443 _ if Some(c) == quote => quote = None,
444 _ => out.push(c),
445 }
446 }
447 out
448}
449
450/// Split a pipeline string into tokens, honoring quoted property values and
451/// `#` comments. Outside quotes, whitespace separates tokens and `!` is a
452/// standalone token; inside a `"..."` or `'...'` region both are literal, so a
453/// value may contain spaces (and even `!`), e.g. `element="x264enc bitrate=4000"`
454/// or `location='/my file.ts'`. A `\` escapes the next character when it is one
455/// of the launch-special ones ([`is_escapable`]), so `location=/my\ file.ts` and
456/// a `\"` inside a quoted region are literal. A `#` outside quotes starts a
457/// comment that runs to end of line (a pasted multi-line pipeline may carry
458/// them). Quotes and escapes are kept on the token; [`unquote_value`] resolves
459/// them once the `key=` split is known. An unterminated quote runs to end of
460/// input (best-effort; the property parse then reports any resulting malformed
461/// token).
462fn tokenize(s: &str) -> Vec<String> {
463 let mut tokens = Vec::new();
464 let mut cur = String::new();
465 // The open quote char (`"` or `'`), or `None` outside a quoted region.
466 let mut quote: Option<char> = None;
467 let mut in_comment = false;
468 let mut chars = s.chars().peekable();
469 while let Some(c) = chars.next() {
470 if in_comment {
471 // A comment runs to end of line; the newline (whitespace) ends it.
472 if c == '\n' {
473 in_comment = false;
474 }
475 continue;
476 }
477 // An escape pair stays whole (and inert) through tokenization: neither
478 // char can close a quote, split a token, or start a comment.
479 if c == '\\' {
480 if let Some(&next) = chars.peek() {
481 if is_escapable(next) {
482 chars.next();
483 cur.push('\\');
484 cur.push(next);
485 continue;
486 }
487 }
488 cur.push('\\');
489 continue;
490 }
491 match c {
492 '"' | '\'' if quote.is_none() => {
493 quote = Some(c);
494 cur.push(c);
495 }
496 _ if Some(c) == quote => {
497 quote = None;
498 cur.push(c);
499 }
500 // A `#` only starts a comment at a token boundary (nothing buffered);
501 // mid-token it is literal, so a URI fragment (`uri=...#closed-captions=cc1`,
502 // `#t=10`) is preserved.
503 '#' if quote.is_none() && cur.is_empty() => {
504 in_comment = true;
505 }
506 '!' if quote.is_none() => {
507 if !cur.is_empty() {
508 tokens.push(core::mem::take(&mut cur));
509 }
510 tokens.push("!".to_string());
511 }
512 c if c.is_whitespace() && quote.is_none() => {
513 if !cur.is_empty() {
514 tokens.push(core::mem::take(&mut cur));
515 }
516 }
517 _ => cur.push(c),
518 }
519 }
520 if !cur.is_empty() {
521 tokens.push(cur);
522 }
523 tokens
524}
525
526/// Split a `gst-launch` pipeline string into chains: runs of nodes linked by `!`,
527/// with branches expressed as separate chains joined through `name=` / `t.`.
528#[cfg(test)]
529fn parse_chains(pipeline: &str) -> Result<Vec<Chain>, ParseError> {
530 // The unit tests exercise the tokenizer alone, with nothing registered: every
531 // bare token is then a property, which is the pre-registry behaviour.
532 parse_chains_with(pipeline, &|_| false)
533}
534
535/// Split a `gst-launch` pipeline string into chains, asking `knows_element`
536/// whether a bare token opens a new chain (see [`consume_element`]).
537fn parse_chains_with(
538 pipeline: &str,
539 knows_element: &dyn Fn(&str) -> bool,
540) -> Result<Vec<Chain>, ParseError> {
541 let trimmed = pipeline.trim();
542 if trimmed.is_empty() {
543 return Err(ParseError::Empty);
544 }
545 // Tokenize quote-aware so a `!` (a standalone token) and whitespace inside a
546 // quoted value are literal, letting a property value carry spaces.
547 let toks = tokenize(trimmed);
548 let mut tokens = toks.iter().map(String::as_str).peekable();
549
550 #[derive(Clone, Copy)]
551 enum St {
552 Start,
553 AfterBang,
554 AfterNode,
555 }
556
557 let mut chains: Vec<Chain> = Vec::new();
558 let mut cur: Chain = Vec::new();
559 let mut st = St::Start;
560
561 loop {
562 match st {
563 St::Start | St::AfterBang => {
564 let after_bang = matches!(st, St::AfterBang);
565 let Some(tok) = tokens.next() else {
566 if after_bang {
567 return Err(ParseError::EmptyStage); // trailing `!`
568 }
569 break;
570 };
571 if tok == "!" {
572 return Err(ParseError::EmptyStage); // leading or doubled `!`
573 }
574 if is_caps_token(tok) {
575 cur.push(Item::Element(ElementSpec {
576 name: "capsfilter".to_string(),
577 props: alloc::vec![("caps".to_string(), tok.to_string())],
578 instance: None,
579 log_category: None,
580 }));
581 st = St::AfterNode;
582 } else if let Some((name, pad)) = split_pad_ref(tok) {
583 cur.push(Item::Ref {
584 name: name.to_string(),
585 pad: pad.to_string(),
586 });
587 if after_bang {
588 // Tail ref (`! t.`): links the upstream node into the
589 // named element and ends the chain.
590 chains.push(core::mem::take(&mut cur));
591 st = St::Start;
592 } else {
593 // Head ref (`t. ! ...`): feeds the chain from it.
594 st = St::AfterNode;
595 }
596 } else {
597 cur.push(Item::Element(consume_element(
598 tok,
599 &mut tokens,
600 knows_element,
601 )?));
602 st = St::AfterNode;
603 }
604 }
605 St::AfterNode => match tokens.peek() {
606 Some(&"!") => {
607 tokens.next();
608 st = St::AfterBang;
609 }
610 Some(_) => {
611 // A node not joined by `!`: the current chain ends here and a
612 // new one starts at this token (reprocessed as a head).
613 chains.push(core::mem::take(&mut cur));
614 st = St::Start;
615 }
616 None => break,
617 },
618 }
619 }
620
621 if !cur.is_empty() {
622 chains.push(cur);
623 }
624 Ok(chains)
625}
626
627/// The property surface a launch node exposes, so one applier serves every node
628/// shape (source, transform / sink, muxer, demuxer, fan-out source) instead of a
629/// copy of the parse-and-set loop per shape.
630trait PropTarget {
631 fn specs(&self) -> &'static [PropertySpec];
632 fn set(&mut self, name: &str, value: PropValue) -> Result<(), PropError>;
633}
634
635macro_rules! impl_prop_target {
636 ($($t:ty),* $(,)?) => {
637 $(impl PropTarget for Box<$t> {
638 fn specs(&self) -> &'static [PropertySpec] {
639 (**self).properties()
640 }
641 fn set(&mut self, name: &str, value: PropValue) -> Result<(), PropError> {
642 (**self).set_property(name, value)
643 }
644 })*
645 };
646}
647
648impl_prop_target!(
649 dyn DynSourceLoop,
650 dyn DynAsyncElement,
651 dyn crate::runtime::DynMultiInputElement,
652 dyn crate::runtime::DynMultiOutputElement,
653 dyn crate::fanout::DynMultiOutputSource,
654);
655
656/// Apply parsed `key=value` props to a node: look each name up in the element's
657/// declared [`PropertySpec`]s, parse the text for that spec (which validates an
658/// enum nick / flag set against its declared values), then set it.
659fn apply_props<T: PropTarget>(
660 target: &mut T,
661 name: &str,
662 props: &[(String, String)],
663) -> Result<(), ParseError> {
664 for (key, raw) in props {
665 let bad_value = || ParseError::BadValue {
666 element: name.into(),
667 key: key.clone(),
668 value: raw.clone(),
669 };
670 let spec = target.specs().iter().find(|s| s.name == key).copied();
671 let value = match spec {
672 Some(spec) => spec.parse_value(raw).map_err(|e| match e {
673 ValueError::Kind(_) => bad_value(),
674 ValueError::Nick(nick) => ParseError::BadEnumValue {
675 element: name.into(),
676 key: key.clone(),
677 value: nick,
678 values: spec.enum_values.unwrap_or(""),
679 },
680 })?,
681 // The element takes names it cannot declare, so there is no kind to
682 // parse for: the text goes on as written for whatever does know it.
683 None if crate::property::takes_undeclared_properties(target.specs()) => {
684 PropValue::Str(raw.clone())
685 }
686 None => {
687 return Err(ParseError::UnknownProperty {
688 element: name.into(),
689 key: key.clone(),
690 })
691 }
692 };
693 target.set(key, value).map_err(|_| bad_value())?;
694 }
695 Ok(())
696}
697
698/// Build the runnable [`Graph`] from parsed chains: flatten elements, resolve
699/// `t.` references into directed links, derive each element's role (and any tee's
700/// fan-out width) from its link degree, then construct and wire the nodes.
701/// `decodebin`: not an element, but a macro that expands, at parse time, into the
702/// chain of decoders / parsers the auto-plug search finds from its upstream caps
703/// down to raw video or audio (M193).
704fn is_decodebin(name: &str) -> bool {
705 matches!(name, "decodebin")
706}
707
708/// Depth bound for the decodebin auto-plug search: a parse + decode (+ a spare
709/// hop) is 2-3, so this leaves headroom without letting an unsatisfiable target
710/// wander.
711const DECODEBIN_MAX_DEPTH: usize = 6;
712
713/// The decode chain to raw for `input`, decoding into `preferred` (the memory
714/// the element right after the macro takes).
715///
716/// The chain is returned built, not named: the caps the search chose a decoder
717/// to produce reach the element only through the factory that takes them, and a
718/// parameterless launch factory would build the decoder's default format again.
719/// That is how `decodebin ! wgpusink` reached a strict-NV12 sink with a decoder
720/// left on I420; the solver re-fixates among the decoder's advertised formats
721/// once the chosen one arrives.
722fn autoplug_for_consumer(
723 registry: &Registry,
724 input: &Caps,
725 preferred: MemoryDomainKind,
726 avoided: &[&str],
727) -> Option<Vec<Box<dyn DynAsyncElement>>> {
728 let raw = |c: &Caps| is_raw_video(c) || is_raw_audio(c);
729 let mut chain =
730 registry.autoplug_avoiding(input, &raw, DECODEBIN_MAX_DEPTH, preferred, avoided)?;
731 // M421/M676: prepend the re-framing parser ahead of a real decode of an
732 // elementary stream, like the boxed `decodebin` splice (the caps-identity
733 // parser is invisible to the shortest-chain search, so it never appears in
734 // the chain).
735 if !chain.is_empty() {
736 if let Some(parser) = registry
737 .parser_name(input)
738 .and_then(|p| registry.make_element(p))
739 {
740 chain.insert(0, parser);
741 }
742 }
743 Some(chain)
744}
745
746/// Expand every `decodebin` node into the decoder chain the registry auto-plugs
747/// from its predecessor's declared caps down to raw (video or audio). An empty
748/// chain (the input is already raw) drops the node entirely, so its predecessor
749/// links straight to its consumer. The predecessor is the element immediately
750/// before the `decodebin` in the same chain; a `decodebin` with no upstream
751/// element (chain head, or after a bare `name.` reference) is a loud error,
752/// since it has nothing to take its input caps from.
753fn expand_decodebin(
754 registry: &Registry,
755 chains: Vec<Chain>,
756 avoided: &[&str],
757) -> Result<Vec<Chain>, ParseError> {
758 // Names referenced as `name.` somewhere: a `decodebin name=d` with such refs is
759 // a FAN-OUT node (M482), not the inline linear case, so it is left unexpanded
760 // here and handled by the decodebin-select path in `build_graph` (which probes
761 // the file, demuxes, and decodes each requested port). Only the unreferenced
762 // inline `... ! decodebin ! ...` expands to a linear decode chain below.
763 let mut referenced: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
764 for chain in &chains {
765 for item in chain {
766 if let Item::Ref { name, .. } = item {
767 referenced.insert(name.clone());
768 }
769 }
770 }
771 let is_fanout_decodebin = |spec: &ElementSpec| {
772 is_decodebin(&spec.name)
773 && spec
774 .instance
775 .as_deref()
776 .map(|n| referenced.contains(n))
777 .unwrap_or(false)
778 };
779
780 let mut out = Vec::with_capacity(chains.len());
781 for chain in chains {
782 let mut new_chain: Chain = Vec::with_capacity(chain.len());
783 // The element (name + props) whose output caps feed the next decodebin:
784 // the most recent real element. A `Ref` clears it (its caps live in
785 // another chain). Props matter because they can re-type the output (a
786 // `filesrc`'s `bytestream-format` selects the container).
787 let mut upstream: Option<(String, Vec<(String, String)>)> = None;
788 let mut items = chain.into_iter().peekable();
789 while let Some(item) = items.next() {
790 match item {
791 // A fan-out `decodebin name=d` is left for `build_graph`'s
792 // decodebin-select path; it is not linearly expandable.
793 Item::Element(spec) if is_fanout_decodebin(&spec) => {
794 upstream = Some((spec.name.clone(), spec.props.clone()));
795 new_chain.push(Item::Element(spec));
796 }
797 Item::Element(spec) if is_decodebin(&spec.name) => {
798 let (pred, props) = upstream.as_ref().ok_or(ParseError::DecodebinNoUpstream)?;
799 let caps = resolve_upstream_caps(registry, pred, props)?;
800 // M746: a single-stream demux fixes its output pad before parsing
801 // any byte, so it defaults to a video port; on an audio-only
802 // container the default auto-plug would pick a video decoder and
803 // fail "no caps overlap". If a primary-stream hook sniffs the file
804 // and names the real (audio) stream, plug that demux with its
805 // stream selection and auto-plug the decoder from the elementary
806 // caps instead. A hook declines a container with a video track (the
807 // default video path is right) or one it does not parse.
808 let location = props
809 .iter()
810 .find(|(k, _)| k == "location")
811 .map(|(_, v)| v.as_str());
812 let chain_input =
813 match location.and_then(|loc| registry.primary_stream(loc, &caps)) {
814 Some(primary) => {
815 new_chain.push(Item::Element(ElementSpec {
816 name: primary.demux.to_string(),
817 props: primary.props.clone(),
818 instance: None,
819 log_category: None,
820 }));
821 primary.caps
822 }
823 None => caps,
824 };
825 // M1018: the element right after the `decodebin` decides what
826 // memory the decoder should decode into, so a GPU-resident
827 // consumer gets a decoder that decodes into its domain rather
828 // than one whose frames have to be downloaded. Only the
829 // immediate consumer counts, the rule the graph-side
830 // derivation follows.
831 let preferred = match items.peek() {
832 Some(Item::Element(consumer)) => {
833 registry.declared_memory_preference(&consumer.name)
834 }
835 _ => MemoryDomainKind::System,
836 };
837 let decoders =
838 autoplug_for_consumer(registry, &chain_input, preferred, avoided)
839 .ok_or_else(|| {
840 ParseError::NoDecodeChain(alloc::format!("{chain_input:?}"))
841 })?;
842 // Built here rather than named, so each element is
843 // constructed for the caps the search chose it to produce (a
844 // multi-format decoder emits the format the consumer takes,
845 // not the first one it lists). A pre-built node carries no
846 // name, so `upstream` clears as it does after a
847 // `uridecodebin`.
848 for element in decoders {
849 new_chain.push(Item::Prebuilt(PrebuiltNode::Element(element)));
850 }
851 upstream = None;
852 }
853 Item::Element(spec) => {
854 upstream = Some((spec.name.clone(), spec.props.clone()));
855 new_chain.push(Item::Element(spec));
856 }
857 Item::Ref { name, pad } => {
858 upstream = None;
859 new_chain.push(Item::Ref { name, pad });
860 }
861 // A pre-built node (from `uridecodebin` / `playbin`) carries no
862 // name to take declared caps from, so a `decodebin` cannot follow
863 // it. Clear the upstream; if a decodebin does follow, it reports
864 // the missing-upstream error.
865 prebuilt @ Item::Prebuilt(_) => {
866 upstream = None;
867 new_chain.push(prebuilt);
868 }
869 }
870 }
871 out.push(new_chain);
872 }
873 Ok(out)
874}
875
876/// `encodebin`: not an element either, but a macro that expands, at parse time,
877/// into one encoder per profile stream plus the muxer that writes the profile's
878/// container (M1089). `encodebin2` is gst's name for the same thing.
879fn is_encodebin(name: &str) -> bool {
880 matches!(name, "encodebin" | "encodebin2")
881}
882
883/// The separator between an encoding profile's container and its streams, the
884/// same string form gst's `GstEncodingProfile` serializes to.
885const PROFILE_SEPARATOR: char = ':';
886
887/// A parsed `profile=`: the container to mux into (`None` for a container-less
888/// profile, which is one coded stream and no muxer), and each stream it carries.
889struct EncodingProfile {
890 container: Option<Caps>,
891 streams: Vec<ProfileStream>,
892}
893
894/// One stream of a profile: the coded form it names, plus whatever its part
895/// pins (M1097). A pinned raw field is honoured by the converter spliced ahead
896/// of the encoder; a pinned bitrate is a property on the encoder itself.
897struct ProfileStream {
898 caps: Caps,
899 width: Option<u32>,
900 height: Option<u32>,
901 /// Frames per second as the `numerator/denominator` `videorate` takes.
902 framerate: Option<(u32, u32)>,
903 sample_rate: Option<u32>,
904 channels: Option<u8>,
905 /// Bits per second, gst's `bitrate` unit.
906 bitrate: Option<u64>,
907}
908
909/// The encoder property a profile's pinned bitrate is applied through.
910const BITRATE_PROPERTY: &str = "bitrate";
911
912/// Parse `container-caps:stream-caps[:stream-caps...]`.
913///
914/// The media type of each part chooses the element: the container names the
915/// muxer, a stream names the codec its encoder must produce. A stream part may
916/// also pin a width / height / framerate (or a sample `rate` / `channels`),
917/// which splices the converter that reaches it, and a `bitrate`, which is set on
918/// the encoder. Any other field is refused rather than dropped.
919fn parse_encoding_profile(text: &str) -> Result<EncodingProfile, ParseError> {
920 let bad = |msg: &str| ParseError::EncodingProfile(msg.to_string());
921 let mut parts = text.split(PROFILE_SEPARATOR).map(str::trim).peekable();
922 let first = *parts
923 .peek()
924 .filter(|p| !p.is_empty())
925 .ok_or_else(|| bad("empty"))?;
926 let head = single_caps(first).ok_or_else(|| {
927 ParseError::EncodingProfile(alloc::format!("unknown media type `{first}`"))
928 })?;
929 // A leading container is the muxing form; a leading stream is gst's
930 // container-less profile, which encodes and stops.
931 let container = match head {
932 Caps::ByteStream { .. } => {
933 parts.next();
934 Some(head)
935 }
936 _ => None,
937 };
938 let mut streams = Vec::new();
939 for part in parts {
940 if part.is_empty() {
941 return Err(bad("empty stream"));
942 }
943 streams.push(parse_profile_stream(part)?);
944 }
945 if streams.is_empty() {
946 return Err(bad("no stream"));
947 }
948 Ok(EncodingProfile { container, streams })
949}
950
951/// Parse one stream part: its media type plus the fields it pins.
952///
953/// The caps parser ignores a field it has no home for, so the pinned settings
954/// are read from the text here instead: an audio caps carries a channel count
955/// and a sample rate whether or not the part named them, and `bitrate` is not a
956/// caps field at all.
957fn parse_profile_stream(part: &str) -> Result<ProfileStream, ParseError> {
958 let caps = single_caps(part)
959 .ok_or_else(|| ParseError::EncodingProfile(alloc::format!("unknown stream `{part}`")))?;
960 let video = matches!(caps, Caps::CompressedVideo { .. } | Caps::RawVideo { .. });
961 let audio = matches!(caps, Caps::Audio { .. });
962 let mut stream = ProfileStream {
963 caps,
964 width: None,
965 height: None,
966 framerate: None,
967 sample_rate: None,
968 channels: None,
969 bitrate: None,
970 };
971 for field in crate::caps_parse::split_top_commas(part)
972 .into_iter()
973 .skip(1)
974 {
975 let (key, value) = field.split_once('=').ok_or_else(|| {
976 ParseError::EncodingProfile(alloc::format!(
977 "`{part}` has a field `{}` that is not key=value",
978 field.trim()
979 ))
980 })?;
981 let (key, value) = (key.trim(), value.trim());
982 match key {
983 "width" if video => {
984 stream.width = Some(profile_number(part, key, value, MAX_PROFILE_VALUE)? as u32)
985 }
986 "height" if video => {
987 stream.height = Some(profile_number(part, key, value, MAX_PROFILE_VALUE)? as u32)
988 }
989 "framerate" if video => stream.framerate = Some(profile_framerate(part, value)?),
990 "rate" if audio => {
991 stream.sample_rate = Some(profile_number(part, key, value, MAX_PROFILE_VALUE)? as u32)
992 }
993 "channels" if audio => {
994 stream.channels = Some(profile_number(part, key, value, MAX_CHANNELS)? as u8)
995 }
996 BITRATE_PROPERTY => {
997 stream.bitrate = Some(profile_number(part, key, value, MAX_PROFILE_VALUE)?)
998 }
999 _ => {
1000 return Err(ParseError::EncodingProfile(alloc::format!(
1001 "`{part}` pins `{key}`, which a profile cannot apply: set it on an element ahead of the encodebin instead"
1002 )))
1003 }
1004 }
1005 }
1006 // The scaler takes a whole output geometry, so half of one has nothing to
1007 // scale to.
1008 if stream.width.is_some() != stream.height.is_some() {
1009 return Err(ParseError::EncodingProfile(alloc::format!(
1010 "`{part}` pins one of width / height: a scale needs both, so pin both or neither"
1011 )));
1012 }
1013 Ok(stream)
1014}
1015
1016/// Upper bounds on a pinned profile field, so a bogus value fails the parse
1017/// rather than reaching an element as a truncated cast. Every field but the
1018/// channel count is a `u32` where it lands.
1019const MAX_PROFILE_VALUE: u64 = u32::MAX as u64;
1020const MAX_CHANNELS: u64 = u8::MAX as u64;
1021
1022/// A pinned profile field's number. Zero is rejected everywhere it appears
1023/// (a zero geometry, rate, channel count or bitrate pins nothing).
1024fn profile_number(part: &str, key: &str, value: &str, max: u64) -> Result<u64, ParseError> {
1025 value
1026 .parse::<u64>()
1027 .ok()
1028 .filter(|n| (1..=max).contains(n))
1029 .ok_or_else(|| {
1030 ParseError::EncodingProfile(alloc::format!(
1031 "`{part}` has a bad `{key}={value}`: expected a whole number 1..={max}"
1032 ))
1033 })
1034}
1035
1036/// A pinned `framerate=N/D` (or a bare `framerate=N`, gst's shorthand), in the
1037/// fraction form `videorate`'s property takes.
1038fn profile_framerate(part: &str, value: &str) -> Result<(u32, u32), ParseError> {
1039 let (numerator, denominator) = value.split_once('/').unwrap_or((value, "1"));
1040 Ok((
1041 profile_number(part, "framerate", numerator.trim(), MAX_PROFILE_VALUE)? as u32,
1042 profile_number(part, "framerate", denominator.trim(), MAX_PROFILE_VALUE)? as u32,
1043 ))
1044}
1045
1046/// One caps from a media-type description. A format-less raw description
1047/// (`video/x-raw`, `audio/x-raw`) expands to every format it could be, and an
1048/// uncompressed profile stream means "store what arrives" rather than any one of
1049/// them, so its first alternative stands for the whole set: the expansion reads
1050/// only which kind of stream it is. `None` when the text names no media type
1051/// this build knows.
1052fn single_caps(desc: &str) -> Option<Caps> {
1053 let set = CapsSet::from_gst_string(desc)?;
1054 match set.alternatives() {
1055 [only] => Some(only.clone()),
1056 [first, ..] if is_raw_video(first) || is_raw_audio(first) => Some(first.clone()),
1057 _ => None,
1058 }
1059}
1060
1061/// The profile stream that encodes `input`: a raw video input takes the first
1062/// coded video stream, raw audio the first coded audio one. `None` when the
1063/// profile carries nothing of that kind.
1064fn stream_for_input<'a>(profile: &'a EncodingProfile, input: &Caps) -> Option<&'a ProfileStream> {
1065 let want_video = is_raw_video(input);
1066 let want_audio = is_raw_audio(input);
1067 profile.streams.iter().find(|stream| match stream.caps {
1068 Caps::CompressedVideo { .. } => want_video,
1069 Caps::Audio { .. } => want_audio,
1070 _ => false,
1071 })
1072}
1073
1074/// The converters an encoder needs ahead of it, when the raw stream reaching it
1075/// is not in a form it takes: a pixel-format convert for video, a sample-format
1076/// convert plus a resampler for audio (either the format or the rate can be the
1077/// mismatch). Asked of the encoder itself rather than guessed from a template,
1078/// so an encoder that already accepts what arrives gets nothing spliced.
1079///
1080/// With no upstream caps to check (a decode chain the parser expanded has no
1081/// name to ask), the converters go in anyway: they pass a matching format
1082/// through, where a missing one fails the negotiation.
1083fn conversion_specs(
1084 registry: &Registry,
1085 encoder: &ElementSpec,
1086 input: Option<&Caps>,
1087 stream: &Caps,
1088) -> Vec<ElementSpec> {
1089 let plain = |name: &str| ElementSpec {
1090 name: name.to_string(),
1091 props: Vec::new(),
1092 instance: None,
1093 log_category: None,
1094 };
1095 if let Some(input) = input {
1096 let accepted = registry
1097 .make_element(&encoder.name)
1098 .is_some_and(|element| element.intercept_caps(input).is_ok());
1099 if accepted {
1100 return Vec::new();
1101 }
1102 }
1103 // Which converters belong to this stream is the stream's own kind: the
1104 // encoder for a video stream takes raw video, whatever reaches it.
1105 match stream {
1106 Caps::CompressedVideo { .. } => Vec::from([plain("videoconvert")]),
1107 Caps::Audio { .. } => Vec::from([plain("audioconvert"), plain("audioresample")]),
1108 _ => Vec::new(),
1109 }
1110}
1111
1112/// The encoder element for one profile stream, as a spec the builder constructs
1113/// like any named element. The properties come from the registry's choice (a
1114/// multi-codec encoder is pinned to this codec with `codec=`), plus the
1115/// profile's own `bitrate` where it pinned one.
1116fn encoder_spec(
1117 registry: &Registry,
1118 stream: &ProfileStream,
1119) -> Result<Option<ElementSpec>, ParseError> {
1120 // An uncompressed stream profile (`audio/x-raw` in a WAV, `video/x-raw` in a
1121 // y4m) stores what arrives, so there is nothing to encode.
1122 if is_raw_video(&stream.caps) || is_raw_audio(&stream.caps) {
1123 if let Some(bitrate) = stream.bitrate {
1124 return Err(ParseError::EncodingProfile(alloc::format!(
1125 "pins `bitrate={bitrate}` on an uncompressed stream, which has no encoder to set it on"
1126 )));
1127 }
1128 return Ok(None);
1129 }
1130 let choice = registry.encoder_choice(&stream.caps).ok_or_else(|| {
1131 ParseError::NoEncoder(alloc::format!(
1132 "no encoder for {} is compiled in (build the feature for it, or name an encoder explicitly)",
1133 stream.caps.to_gst_string()
1134 ))
1135 })?;
1136 let mut props: Vec<(String, String)> = choice
1137 .props
1138 .iter()
1139 .map(|(k, v)| (k.to_string(), v.to_string()))
1140 .collect();
1141 if let Some(bitrate) = stream.bitrate {
1142 if !declares_bitrate(registry, choice.element) {
1143 return Err(ParseError::EncodingProfile(alloc::format!(
1144 "pins `bitrate={bitrate}`, which `{}` has no property for: drop it, or name the encoder yourself with the knob it does have",
1145 choice.element
1146 )));
1147 }
1148 props.push((BITRATE_PROPERTY.to_string(), alloc::format!("{bitrate}")));
1149 }
1150 Ok(Some(ElementSpec {
1151 name: choice.element.to_string(),
1152 props,
1153 instance: None,
1154 log_category: None,
1155 }))
1156}
1157
1158/// Whether an encoder takes a target bitrate at all, asked of the element rather
1159/// than assumed, so a profile that pins one on a fixed-rate encoder fails the
1160/// parse instead of encoding at some other rate.
1161fn declares_bitrate(registry: &Registry, element: &str) -> bool {
1162 registry.make_element(element).is_some_and(|element| {
1163 element
1164 .properties()
1165 .iter()
1166 .any(|spec| spec.name == BITRATE_PROPERTY)
1167 })
1168}
1169
1170/// The elements one branch of a profile expands to: the converters its encoder
1171/// needs, the converters its pinned settings need, then the encoder itself. A
1172/// pinned setting on an uncompressed stream still gets its converter, there is
1173/// just nothing after it but the muxer.
1174fn stream_specs(
1175 registry: &Registry,
1176 stream: &ProfileStream,
1177 upstream: Option<&(String, Vec<(String, String)>)>,
1178) -> Result<Vec<ElementSpec>, ParseError> {
1179 let encoder = encoder_spec(registry, stream)?;
1180 let mut specs = match &encoder {
1181 Some(encoder) => {
1182 let input = upstream
1183 .and_then(|(name, props)| resolve_upstream_caps(registry, name, props).ok());
1184 conversion_specs(registry, encoder, input.as_ref(), &stream.caps)
1185 }
1186 None => Vec::new(),
1187 };
1188 splice_pinned_settings(&mut specs, stream);
1189 specs.extend(encoder);
1190 Ok(specs)
1191}
1192
1193/// Apply a stream's pinned settings to the converters ahead of its encoder: a
1194/// geometry sets `videoscale`, a framerate `videorate`, a channel count
1195/// `audioconvert` and a sample rate `audioresample`, adding that converter when
1196/// the encoder did not already need one.
1197fn splice_pinned_settings(specs: &mut Vec<ElementSpec>, stream: &ProfileStream) {
1198 if let (Some(width), Some(height)) = (stream.width, stream.height) {
1199 pin_on_converter(specs, "videoscale", "width", alloc::format!("{width}"));
1200 pin_on_converter(specs, "videoscale", "height", alloc::format!("{height}"));
1201 }
1202 if let Some((numerator, denominator)) = stream.framerate {
1203 pin_on_converter(
1204 specs,
1205 "videorate",
1206 "framerate",
1207 alloc::format!("{numerator}/{denominator}"),
1208 );
1209 }
1210 if let Some(channels) = stream.channels {
1211 pin_on_converter(
1212 specs,
1213 "audioconvert",
1214 "channels",
1215 alloc::format!("{channels}"),
1216 );
1217 }
1218 if let Some(sample_rate) = stream.sample_rate {
1219 pin_on_converter(
1220 specs,
1221 "audioresample",
1222 "samplerate",
1223 alloc::format!("{sample_rate}"),
1224 );
1225 }
1226}
1227
1228/// Set a property on the named converter, appending the converter when the list
1229/// does not already carry it.
1230fn pin_on_converter(specs: &mut Vec<ElementSpec>, element: &str, key: &str, value: String) {
1231 match specs.iter_mut().find(|spec| spec.name == element) {
1232 Some(spec) => spec.props.push((key.to_string(), value)),
1233 None => specs.push(ElementSpec {
1234 name: element.to_string(),
1235 props: Vec::from([(key.to_string(), value)]),
1236 instance: None,
1237 log_category: None,
1238 }),
1239 }
1240}
1241
1242/// `transcodebin`: decode whatever arrives and re-encode it to a profile, which
1243/// is exactly `decodebin ! encodebin profile=...`. Rewritten into those two
1244/// macros before either expands, so it inherits both behaviours (and both error
1245/// messages) rather than repeating them.
1246fn expand_transcodebin(chains: Vec<Chain>) -> Vec<Chain> {
1247 let mut out = Vec::with_capacity(chains.len());
1248 for chain in chains {
1249 let mut new_chain: Chain = Vec::with_capacity(chain.len() + 1);
1250 for item in chain {
1251 match item {
1252 Item::Element(spec) if spec.name == "transcodebin" => {
1253 new_chain.push(Item::Element(ElementSpec {
1254 name: "decodebin".to_string(),
1255 props: Vec::new(),
1256 instance: None,
1257 log_category: spec.log_category.clone(),
1258 }));
1259 new_chain.push(Item::Element(ElementSpec {
1260 name: "encodebin".to_string(),
1261 ..spec
1262 }));
1263 }
1264 other => new_chain.push(other),
1265 }
1266 }
1267 out.push(new_chain);
1268 }
1269 out
1270}
1271
1272/// Expand every `encodebin` into `<encoder> ! <muxer>`: the encoder for the
1273/// stream matching that chain's input, and the muxer for the profile's container.
1274/// The muxer inherits the bin's `name=`, so a second branch reaching it as `e.`
1275/// links into the muxer the way any fan-in does; each such branch gets the
1276/// encoder for its own kind of input inserted ahead of the reference.
1277fn expand_encodebin(registry: &Registry, chains: Vec<Chain>) -> Result<Vec<Chain>, ParseError> {
1278 // Nothing to do for the common line with no encodebin in it.
1279 if !chains
1280 .iter()
1281 .flatten()
1282 .any(|item| matches!(item, Item::Element(spec) if is_encodebin(&spec.name)))
1283 {
1284 return Ok(chains);
1285 }
1286
1287 // Every encodebin's profile and instance name, so a branch referencing the
1288 // name knows which profile to encode for.
1289 let mut profiles: Vec<(Option<String>, EncodingProfile)> = Vec::new();
1290 for chain in &chains {
1291 for item in chain {
1292 if let Item::Element(spec) = item {
1293 if is_encodebin(&spec.name) {
1294 let text = prop(spec, "profile")
1295 .ok_or_else(|| ParseError::EncodingProfile("missing".to_string()))?;
1296 for (key, _) in &spec.props {
1297 if !matches!(key.as_str(), "profile") {
1298 return Err(ParseError::EncodingProfile(alloc::format!(
1299 "unknown property `{key}`"
1300 )));
1301 }
1302 }
1303 profiles.push((spec.instance.clone(), parse_encoding_profile(text)?));
1304 }
1305 }
1306 }
1307 }
1308
1309 let mut out = Vec::with_capacity(chains.len());
1310 for chain in chains {
1311 let mut new_chain: Chain = Vec::with_capacity(chain.len() + 1);
1312 let mut upstream: Option<(String, Vec<(String, String)>)> = None;
1313 for item in chain {
1314 match item {
1315 Item::Element(spec) if is_encodebin(&spec.name) => {
1316 let profile = profiles
1317 .iter()
1318 .find(|(instance, _)| *instance == spec.instance)
1319 .map(|(_, profile)| profile)
1320 .expect("every encodebin's profile was parsed above");
1321 // The container is the profile's head, so a missing muxer is
1322 // reported before a missing encoder.
1323 let muxer = muxer_spec(registry, profile, &spec)?;
1324 let stream = profile_stream(registry, profile, upstream.as_ref())?;
1325 for element in stream_specs(registry, stream, upstream.as_ref())? {
1326 new_chain.push(Item::Element(element));
1327 }
1328 if let Some(muxer) = muxer {
1329 new_chain.push(Item::Element(muxer));
1330 }
1331 upstream = None;
1332 }
1333 // A branch feeding an encodebin by name encodes its own input
1334 // first: the reference links into the muxer, which takes coded
1335 // streams.
1336 Item::Ref { name, pad } => {
1337 if let Some((_, profile)) = profiles
1338 .iter()
1339 .find(|(instance, _)| instance.as_deref() == Some(name.as_str()))
1340 {
1341 let stream = profile_stream(registry, profile, upstream.as_ref())?;
1342 for element in stream_specs(registry, stream, upstream.as_ref())? {
1343 new_chain.push(Item::Element(element));
1344 }
1345 }
1346 upstream = None;
1347 new_chain.push(Item::Ref { name, pad });
1348 }
1349 Item::Element(spec) => {
1350 upstream = Some((spec.name.clone(), spec.props.clone()));
1351 new_chain.push(Item::Element(spec));
1352 }
1353 prebuilt @ Item::Prebuilt(_) => {
1354 upstream = None;
1355 new_chain.push(prebuilt);
1356 }
1357 }
1358 }
1359 out.push(new_chain);
1360 }
1361 Ok(out)
1362}
1363
1364/// The profile stream a branch encodes into. A single-stream profile leaves no
1365/// choice, so it needs no input caps at all (which is what lets `decodebin !
1366/// encodebin` work: an expanded decode chain has no name to ask for caps). With
1367/// several streams the branch's own input decides which one, so the element
1368/// ahead of it has to declare its output.
1369fn profile_stream<'a>(
1370 registry: &Registry,
1371 profile: &'a EncodingProfile,
1372 upstream: Option<&(String, Vec<(String, String)>)>,
1373) -> Result<&'a ProfileStream, ParseError> {
1374 if let [only] = &profile.streams[..] {
1375 return Ok(only);
1376 }
1377 let input = encodebin_input_caps(registry, upstream)?;
1378 stream_for_input(profile, &input).ok_or_else(|| {
1379 ParseError::EncodebinInput(alloc::format!(
1380 "the profile carries no stream that encodes {}",
1381 input.to_gst_string()
1382 ))
1383 })
1384}
1385
1386/// The raw caps reaching an `encodebin` (or a branch that references one): the
1387/// declared output of the element ahead of it.
1388fn encodebin_input_caps(
1389 registry: &Registry,
1390 upstream: Option<&(String, Vec<(String, String)>)>,
1391) -> Result<Caps, ParseError> {
1392 let (name, props) = upstream.ok_or_else(|| {
1393 ParseError::EncodebinInput(
1394 "no upstream element to encode; it must follow a source or element with declared caps"
1395 .to_string(),
1396 )
1397 })?;
1398 resolve_upstream_caps(registry, name, props).map_err(|_| {
1399 ParseError::EncodebinInput(alloc::format!("`{name}` declares no output caps to encode"))
1400 })
1401}
1402
1403/// The muxer element for a profile's container, carrying the bin's instance name
1404/// so a `name.` reference resolves to it.
1405fn muxer_spec(
1406 registry: &Registry,
1407 profile: &EncodingProfile,
1408 bin: &ElementSpec,
1409) -> Result<Option<ElementSpec>, ParseError> {
1410 let Some(container) = &profile.container else {
1411 return Ok(None);
1412 };
1413 let name = registry.muxer_name(container).ok_or_else(|| {
1414 ParseError::NoEncoder(alloc::format!(
1415 "no muxer for {} is compiled in",
1416 container.to_gst_string()
1417 ))
1418 })?;
1419 Ok(Some(ElementSpec {
1420 name: name.to_string(),
1421 props: Vec::new(),
1422 instance: bin.instance.clone(),
1423 log_category: bin.log_category.clone(),
1424 }))
1425}
1426
1427/// The caps a `decodebin` predecessor produces, used as the auto-plug input
1428/// (M195). For a registered source, build it and apply its properties so a
1429/// property that re-types the output (a `filesrc`'s `bytestream-format`) is
1430/// reflected via [`SourceLoop::probe_output_caps`]; fall back to the registry's
1431/// declared caps (a fixed source, or a transform's source-pad template).
1432/// `probe_output_caps` also sniffs a `bytestream-format=auto` source's header at
1433/// parse time (M480), so `decodebin` picks the demuxer from the real content even
1434/// when the file extension is wrong; only an unreadable / unrecognized file falls
1435/// back to the declared default.
1436fn resolve_upstream_caps(
1437 registry: &Registry,
1438 name: &str,
1439 props: &[(String, String)],
1440) -> Result<Caps, ParseError> {
1441 if let Some(mut src) = registry.make_source(name) {
1442 apply_props(&mut src, name, props)?;
1443 // `probe_output_caps` may sniff the header (a `bytestream-format=auto`
1444 // source), so `decodebin` picks the demuxer from the real content, not a
1445 // mislabeled extension; it falls back to the no-I/O caps otherwise.
1446 if let Some(caps) = src.probe_output_caps() {
1447 return Ok(caps);
1448 }
1449 }
1450 registry
1451 .declared_output_caps(name)
1452 .ok_or(ParseError::DecodebinNoUpstream)
1453}
1454
1455/// `uridecodebin` / `playbin`: a source-providing macro. `uridecodebin uri=X`
1456/// builds the source from the URI scheme handler and auto-plugs the decode chain
1457/// to raw; `playbin uri=X` is that plus an auto sink (`autovideosink`, or the
1458/// `video-sink=` override), i.e. a complete pipeline.
1459fn is_uri_source(name: &str) -> bool {
1460 matches!(name, "uridecodebin" | "playbin")
1461}
1462
1463/// The value of a spec property by key, if present.
1464fn prop<'a>(spec: &'a ElementSpec, key: &str) -> Option<&'a str> {
1465 spec.props
1466 .iter()
1467 .find(|(k, _)| k == key)
1468 .map(|(_, v)| v.as_str())
1469}
1470
1471/// Expand every `uridecodebin` / `playbin` (a source position element) into the
1472/// `uri=` scheme handler's source plus the auto-plugged decode chain, as
1473/// pre-built nodes spliced straight into the chain. `playbin` additionally
1474/// appends an auto sink so the line is a complete pipeline. The element must head
1475/// its chain (it provides the source).
1476fn expand_uri_sources(
1477 registry: &Registry,
1478 chains: Vec<Chain>,
1479 avoided: &[&str],
1480) -> Result<Vec<Chain>, ParseError> {
1481 let mut out = Vec::with_capacity(chains.len());
1482 for chain in chains {
1483 let mut new_chain: Chain = Vec::with_capacity(chain.len());
1484 let mut items = chain.into_iter().enumerate().peekable();
1485 while let Some((i, item)) = items.next() {
1486 let spec = match item {
1487 Item::Element(spec) if is_uri_source(&spec.name) => spec,
1488 other => {
1489 new_chain.push(other);
1490 continue;
1491 }
1492 };
1493 if i != 0 {
1494 return Err(ParseError::UriSourceNotAtHead(spec.name));
1495 }
1496 let is_playbin = spec.name.starts_with("playbin");
1497 let uri =
1498 prop(&spec, "uri").ok_or_else(|| ParseError::MissingUri(spec.name.clone()))?;
1499 let (source, caps) = registry
1500 .build_uri_source(uri)
1501 .map_err(|e: UriError| ParseError::Uri(alloc::format!("{uri}: {e:?}")))?;
1502 // `playbin` names its own sink, a bare `uridecodebin` is followed by
1503 // one: either way the consumer's declared input memory picks the
1504 // decoder (M1018), as it does after a `decodebin`.
1505 let sink = is_playbin.then(|| {
1506 prop(&spec, "video-sink")
1507 .unwrap_or("autovideosink")
1508 .to_string()
1509 });
1510 let consumer = match (&sink, items.peek()) {
1511 (Some(sink), _) => Some(sink.as_str()),
1512 (None, Some((_, Item::Element(consumer)))) => Some(consumer.name.as_str()),
1513 _ => None,
1514 };
1515 let preferred = consumer.map_or(MemoryDomainKind::System, |name| {
1516 registry.declared_memory_preference(name)
1517 });
1518 let target = |c: &Caps| is_raw_video(c) || is_raw_audio(c);
1519 let decoders = registry
1520 .autoplug_avoiding(&caps, &target, DECODEBIN_MAX_DEPTH, preferred, avoided)
1521 .ok_or_else(|| ParseError::NoDecodeChain(alloc::format!("{caps:?}")))?;
1522 new_chain.push(Item::Prebuilt(PrebuiltNode::Source(source)));
1523 for dec in decoders {
1524 new_chain.push(Item::Prebuilt(PrebuiltNode::Element(dec)));
1525 }
1526 if let Some(sink) = sink {
1527 new_chain.push(Item::Element(ElementSpec {
1528 name: sink,
1529 props: Vec::new(),
1530 instance: None,
1531 log_category: None,
1532 }));
1533 }
1534 }
1535 out.push(new_chain);
1536 }
1537 Ok(out)
1538}
1539
1540fn build_graph(
1541 registry: &Registry,
1542 chains: Vec<Chain>,
1543 avoided: &[&str],
1544) -> Result<Graph<GraphNode>, ParseError> {
1545 // Expand the source-providing (uridecodebin / playbin) and mid-chain
1546 // (decodebin) macros into concrete nodes before the structural build, so the
1547 // rest of the builder sees only real elements and pre-built nodes.
1548 let chains = expand_transcodebin(chains);
1549 let chains = expand_uri_sources(registry, chains, avoided)?;
1550 let chains = expand_decodebin(registry, chains, avoided)?;
1551 let chains = expand_encodebin(registry, chains)?;
1552
1553 // A chain endpoint after flattening: a concrete element index, or a still
1554 // unresolved reference by name.
1555 enum Endpoint {
1556 Element(usize),
1557 Ref { name: String, pad: String },
1558 }
1559
1560 let mut specs: Vec<ElementSpec> = Vec::new();
1561 // Parallel to `specs`: the pre-built node for that index (a `uridecodebin` /
1562 // `playbin` source or decoder), or `None` for a normal name-built element.
1563 // The placeholder spec for a pre-built node carries a benign name so the
1564 // structural closures (`is_queue` / `is_tee`) never match it, and node
1565 // construction uses the pre-built node instead of looking the name up.
1566 let mut prebuilt: Vec<Option<PrebuiltNode>> = Vec::new();
1567 let mut names: Vec<(String, usize)> = Vec::new();
1568 let mut chain_eps: Vec<Vec<Endpoint>> = Vec::with_capacity(chains.len());
1569
1570 for chain in chains {
1571 let mut eps = Vec::with_capacity(chain.len());
1572 for item in chain {
1573 match item {
1574 Item::Element(spec) => {
1575 let ei = specs.len();
1576 if let Some(inst) = &spec.instance {
1577 if names.iter().any(|(n, _)| n == inst) {
1578 return Err(ParseError::DuplicateName(inst.clone()));
1579 }
1580 names.push((inst.clone(), ei));
1581 }
1582 specs.push(spec);
1583 prebuilt.push(None);
1584 eps.push(Endpoint::Element(ei));
1585 }
1586 Item::Prebuilt(node) => {
1587 let ei = specs.len();
1588 let name = match node {
1589 PrebuiltNode::Source(_) => "uridecodebin",
1590 PrebuiltNode::Element(_) => "(decoder)",
1591 };
1592 specs.push(ElementSpec {
1593 name: name.to_string(),
1594 props: Vec::new(),
1595 instance: None,
1596 log_category: None,
1597 });
1598 prebuilt.push(Some(node));
1599 eps.push(Endpoint::Element(ei));
1600 }
1601 Item::Ref { name, pad } => eps.push(Endpoint::Ref { name, pad }),
1602 }
1603 }
1604 chain_eps.push(eps);
1605 }
1606
1607 if specs.len() < 2 {
1608 return Err(ParseError::TooFewStages);
1609 }
1610
1611 // Resolve references, collect the directed links (by element index), and
1612 // record each demux output-pad request (M476): a head-ref `d.video_0` that
1613 // sources a link contributes a `PadRequest` to `demux_pads[d]`, in link (port)
1614 // order, so a demux-select hook can map port i to the requested stream.
1615 let mut raw_links: Vec<(usize, usize)> = Vec::new();
1616 let mut demux_pads: Vec<Vec<PadRequest>> = alloc::vec![Vec::new(); specs.len()];
1617 // The DESTINATION pad request per raw link (M481): a named input-pad ref
1618 // (`... ! mux.audio_0`) carries a request; a bare `mux.` or an inline consumer
1619 // carries `None` (positional). The transpose of `demux_pads` (output side).
1620 let mut raw_dest_req: Vec<Option<PadRequest>> = Vec::new();
1621 for eps in &chain_eps {
1622 let mut idxs: Vec<usize> = Vec::with_capacity(eps.len());
1623 // The pad suffix of each endpoint that is a reference (`None` for a
1624 // concrete element), parallel to `idxs`.
1625 let mut pads: Vec<Option<&str>> = Vec::with_capacity(eps.len());
1626 for ep in eps {
1627 match ep {
1628 Endpoint::Element(ei) => {
1629 idxs.push(*ei);
1630 pads.push(None);
1631 }
1632 Endpoint::Ref { name, pad } => {
1633 let i = names
1634 .iter()
1635 .find(|(n, _)| n == name)
1636 .map(|(_, i)| *i)
1637 .ok_or_else(|| ParseError::UnknownReference(name.clone()))?;
1638 idxs.push(i);
1639 pads.push(Some(pad.as_str()));
1640 }
1641 }
1642 }
1643 for w in 0..idxs.len().saturating_sub(1) {
1644 let (s, d) = (idxs[w], idxs[w + 1]);
1645 raw_links.push((s, d));
1646 // Record the source's output-pad request in port order: a pad-ref
1647 // source (`d.video_0`) carries a named request; an inline output
1648 // (`d ! x`) takes the positional Nth-forwardable-stream request. Only
1649 // consulted for an explicit-demux fan-out node (M476); harmless noise
1650 // for a normal element or a tee.
1651 let ordinal = demux_pads[s].len();
1652 let req = match pads[w] {
1653 Some(pad) => parse_pad_request(pad, ordinal),
1654 None => PadRequest {
1655 kind: PadKind::Any,
1656 index: ordinal,
1657 },
1658 };
1659 demux_pads[s].push(req);
1660 // The destination's input-pad request: a named ref (`mux.audio_0`)
1661 // parses; a bare `mux.` (empty suffix) or an inline consumer is `None`
1662 // (positional, resolved by the sequential input counter below).
1663 raw_dest_req.push(match pads[w + 1] {
1664 Some(pad) if !pad.is_empty() => Some(parse_pad_request(pad, 0)),
1665 _ => None,
1666 });
1667 }
1668 }
1669
1670 // M190: `queue` / `queue2` is not an element in g2g. Per the design,
1671 // per-edge `LinkPolicy` (Block / DropOldest / DropNewest) is the leaky-queue
1672 // analog, so a queue node collapses into the backpressure policy of the edge
1673 // it sits on rather than becoming a buffering element + extra hop. Validate
1674 // each queue is 1-in/1-out, read its `leaky=`, then contract it out of the
1675 // link list, walking chains of queues to the first real consumer and keeping
1676 // the downstream-most leaky policy.
1677 let is_queue = |ei: usize| matches!(specs[ei].name.as_str(), "queue" | "queue2");
1678 let mut raw_in = alloc::vec![0usize; specs.len()];
1679 let mut raw_out = alloc::vec![0usize; specs.len()];
1680 for &(s, d) in &raw_links {
1681 raw_out[s] += 1;
1682 raw_in[d] += 1;
1683 }
1684 let mut queue_succ: Vec<Option<usize>> = alloc::vec![None; specs.len()];
1685 let mut queue_policy = alloc::vec![LinkPolicy::Block; specs.len()];
1686 let mut queue_capacity: Vec<Option<usize>> = alloc::vec![None; specs.len()];
1687 for ei in 0..specs.len() {
1688 if is_queue(ei) {
1689 if raw_in[ei] != 1 || raw_out[ei] != 1 {
1690 return Err(ParseError::QueueRole(specs[ei].name.clone()));
1691 }
1692 queue_policy[ei] = queue_leaky_policy(&specs[ei]);
1693 queue_capacity[ei] = queue_capacity_of(&specs[ei]);
1694 queue_succ[ei] = raw_links.iter().find(|(s, _)| *s == ei).map(|(_, d)| *d);
1695 }
1696 }
1697 // Each edge whose source is a real element walks through any run of queues to
1698 // its terminal consumer, carrying the accumulated policy; edges out of a queue
1699 // are consumed by that walk (skipped here).
1700 let mut links: Vec<(usize, usize, LinkPolicy, Option<usize>)> = Vec::new();
1701 // The destination input-pad request per contracted link (M481), aligned with
1702 // `links`; taken from the raw link that lands on the terminal consumer (so a
1703 // `... ! queue ! mux.audio_0` keeps its named pad through the queue contraction).
1704 let mut link_dest_req: Vec<Option<PadRequest>> = Vec::new();
1705 for (li, &(s, d)) in raw_links.iter().enumerate() {
1706 if is_queue(s) {
1707 continue;
1708 }
1709 let (mut cur, mut src_li) = (d, li);
1710 let mut policy = LinkPolicy::Block;
1711 let mut capacity: Option<usize> = None;
1712 while is_queue(cur) {
1713 if queue_policy[cur] != LinkPolicy::Block {
1714 policy = queue_policy[cur];
1715 }
1716 // A run of queues carries the last explicit depth (rare to chain them).
1717 if let Some(c) = queue_capacity[cur] {
1718 capacity = Some(c);
1719 }
1720 let next = queue_succ[cur].expect("queue validated 1-out above");
1721 // The named-pad suffix lives on the raw link that enters the terminal
1722 // consumer, i.e. `(cur -> next)`; find it for the request.
1723 src_li = raw_links
1724 .iter()
1725 .position(|&(a, b)| a == cur && b == next)
1726 .unwrap_or(src_li);
1727 cur = next;
1728 }
1729 links.push((s, cur, policy, capacity));
1730 link_dest_req.push(raw_dest_req[src_li].clone());
1731 }
1732
1733 // Link degree per element fixes its role and any tee's output width. Computed
1734 // over the contracted links, so queue indices drop to degree 0 and are skipped
1735 // as nodes below.
1736 let mut in_deg = alloc::vec![0usize; specs.len()];
1737 let mut out_deg = alloc::vec![0usize; specs.len()];
1738 for &(s, d, _, _) in &links {
1739 out_deg[s] += 1;
1740 in_deg[d] += 1;
1741 }
1742
1743 let is_tee = |ei: usize| specs[ei].name == "tee";
1744 // A non-tee node with several inbound links is a muxer (built from the
1745 // registry with that input count); a tee has a single input pad.
1746 let is_muxer = |ei: usize| !is_tee(ei) && in_deg[ei] > 1;
1747 // A node registered as a demuxer with several outbound links is a fan-out
1748 // demux (M210): the transpose of a muxer. A registered name with one output
1749 // falls back to its single-output launch element (e.g. `tsdemux`), the way a
1750 // one-input muxer name falls back to its single-input element.
1751 let is_demux = |ei: usize| !is_tee(ei) && out_deg[ei] > 1 && registry.is_demux(&specs[ei].name);
1752 // Explicit-demux fan-out (M476): a non-tee, non-registered-demux element that
1753 // fans out to several pads and is fed by a file source is built by a registered
1754 // demux-select hook, which probes the file (`location=`) and returns a
1755 // multi-output demuxer with one port per pad request (in reference order). This
1756 // is how `matroskademux` / `tsdemux` / `qtdemux` in a launch line split a file
1757 // into its elementary streams, honoring `d.video_0` / `d.audio_0` selection.
1758 let mut demux_select_node: Vec<Option<Box<dyn crate::runtime::DynMultiOutputElement>>> =
1759 (0..specs.len()).map(|_| None).collect();
1760 if !registry.demux_select_hooks().is_empty() {
1761 for ei in 0..specs.len() {
1762 if is_tee(ei) || is_demux(ei) || out_deg[ei] <= 1 || prebuilt[ei].is_some() {
1763 continue;
1764 }
1765 // The upstream file location (the source linking into this demux).
1766 let Some(location) = links
1767 .iter()
1768 .find(|(_, d, _, _)| *d == ei)
1769 .and_then(|(s, _, _, _)| prop(&specs[*s], "location"))
1770 else {
1771 continue;
1772 };
1773 for hook in registry.demux_select_hooks() {
1774 if let Some(demux) = hook(&specs[ei].name, location, &demux_pads[ei]) {
1775 demux_select_node[ei] = Some(demux);
1776 break;
1777 }
1778 }
1779 }
1780 }
1781 // `decodebin name=d` fan-out (M482): a `decodebin` node left unexpanded (it has
1782 // named refs) with a file source upstream probes the file, builds the
1783 // multi-output demuxer (stored like a demux-select node so it fans out on its
1784 // own pads), and records each port's elementary caps so the wiring below splices
1785 // a decoder onto every port (the decode-per-port that makes it `decodebin`, not a
1786 // bare demuxer). Declining hooks leave it unbuilt (a loud error at node build).
1787 let mut decode_fanout_caps: Vec<Option<Vec<Caps>>> = (0..specs.len()).map(|_| None).collect();
1788 if !registry.decodebin_select_hooks().is_empty() {
1789 for ei in 0..specs.len() {
1790 if !is_decodebin(&specs[ei].name) || out_deg[ei] == 0 || prebuilt[ei].is_some() {
1791 continue;
1792 }
1793 let Some(location) = links
1794 .iter()
1795 .find(|(_, d, _, _)| *d == ei)
1796 .and_then(|(s, _, _, _)| prop(&specs[*s], "location"))
1797 else {
1798 continue;
1799 };
1800 for hook in registry.decodebin_select_hooks() {
1801 if let Some((demux, caps)) = hook(location, &demux_pads[ei]) {
1802 demux_select_node[ei] = Some(demux);
1803 decode_fanout_caps[ei] = Some(caps);
1804 break;
1805 }
1806 }
1807 }
1808 }
1809 let is_select: Vec<bool> = demux_select_node.iter().map(|d| d.is_some()).collect();
1810 // Auto-tee (M473): a non-tee, non-demux node whose single output fans out to
1811 // several consumers gets an implicit `tee` spliced in below, so a gst-launch
1812 // line that omits the explicit tee still builds. `tee`, registered demuxers,
1813 // and explicit-demux fan-out nodes fan out on their own pads and are left alone.
1814 let needs_tee = |ei: usize| {
1815 !is_tee(ei)
1816 && !is_demux(ei)
1817 && !is_select[ei]
1818 && !registry.is_fanout_src(&specs[ei].name)
1819 && out_deg[ei] > 1
1820 };
1821 // A fan-in element with no output is checked at construction below: a
1822 // terminal session (`is_terminal`) legally ends the graph (M713), a
1823 // merging muxer without a downstream stays `MuxerWithoutOutput`.
1824 for (ei, spec) in specs.iter().enumerate() {
1825 if is_tee(ei) && !spec.props.is_empty() {
1826 // The structural tee carries no element, so it has no properties.
1827 return Err(ParseError::UnknownProperty {
1828 element: "tee".to_string(),
1829 key: spec.props[0].0.clone(),
1830 });
1831 }
1832 }
1833
1834 // Construct nodes in element-index order so `node_of[ei]` lines up. Queue
1835 // indices were contracted into edge policies above, so they get no node
1836 // (`None`); they never appear as an endpoint in the contracted links.
1837 let mut graph: Graph<GraphNode> = Graph::new();
1838 let mut node_of: Vec<Option<NodeId>> = Vec::with_capacity(specs.len());
1839 // The resolved muxer input-pad index per contracted link (M481), aligned with
1840 // `links`; filled at muxer construction from the element's `input_pad_index`
1841 // scheme. `None` for a non-muxer link or an unnamed ref (sequential fallback).
1842 let mut mux_pad_of_link: Vec<Option<u8>> = alloc::vec![None; links.len()];
1843 for ei in 0..specs.len() {
1844 if is_queue(ei) {
1845 node_of.push(None);
1846 continue;
1847 }
1848 // A pre-built node (uridecodebin / playbin source or decoder) is spliced
1849 // in directly; its role still follows link degree (a source has no input,
1850 // a terminal decoder no output).
1851 if let Some(node) = prebuilt[ei].take() {
1852 let nid = match node {
1853 PrebuiltNode::Source(src) => graph.add_source(GraphNodeRef::Source(src)),
1854 PrebuiltNode::Element(el) if out_deg[ei] == 0 => {
1855 graph.add_sink(GraphNodeRef::Element(el))
1856 }
1857 PrebuiltNode::Element(el) => graph.add_transform(GraphNodeRef::Element(el)),
1858 };
1859 node_of.push(Some(nid));
1860 continue;
1861 }
1862 let spec = &specs[ei];
1863 let node = if is_tee(ei) {
1864 graph.add_tee(out_deg[ei] as u8).node()
1865 } else if in_deg[ei] == 0 && registry.is_fanout_src(&spec.name) {
1866 // Terminal fan-out source (M727): 0 inputs, one output per named
1867 // pad reference (`s. ! ...`). The element's intrinsic port count
1868 // must match the linked outputs.
1869 let mut src = registry
1870 .make_fanout_src(&spec.name, out_deg[ei])
1871 .ok_or_else(|| ParseError::UnknownElement(spec.name.clone()))?;
1872 if src.output_count() != out_deg[ei] {
1873 return Err(ParseError::UnknownInputPad(spec.name.clone()));
1874 }
1875 apply_props(&mut src, &spec.name, &spec.props)?;
1876 graph
1877 .add_fanout_src(GraphNodeRef::FanoutSource(src), out_deg[ei] as u8)
1878 .node()
1879 } else if in_deg[ei] == 0 {
1880 let mut src = registry
1881 .make_source(&spec.name)
1882 .ok_or_else(|| ParseError::UnknownSource(spec.name.clone()))?;
1883 apply_props(&mut src, &spec.name, &spec.props)?;
1884 graph.add_source(GraphNodeRef::Source(src))
1885 } else if is_muxer(ei) {
1886 let mut mux = registry
1887 .make_muxer(&spec.name, in_deg[ei])
1888 .ok_or_else(|| ParseError::NotAMuxer(spec.name.clone()))?;
1889 apply_props(&mut mux, &spec.name, &spec.props)?;
1890 // Resolve named input-pad refs (M481) to concrete indices via the
1891 // muxer's own scheme, so `... ! mux.audio_0 ... ! mux.video_0` routes
1892 // by name regardless of order. Named refs claim their index first;
1893 // bare refs fill the remaining slots in link order (the historical
1894 // positional behavior).
1895 let n = in_deg[ei];
1896 let incoming: Vec<usize> = links
1897 .iter()
1898 .enumerate()
1899 .filter(|(_, (_, d, _, _))| *d == ei)
1900 .map(|(k, _)| k)
1901 .collect();
1902 let mut used = alloc::vec![false; n];
1903 for (ord, &k) in incoming.iter().enumerate() {
1904 let Some(req) = &link_dest_req[k] else {
1905 continue;
1906 };
1907 let idx = mux
1908 .input_pad_index(req, ord)
1909 .filter(|&i| i < n)
1910 .ok_or_else(|| ParseError::UnknownInputPad(spec.name.clone()))?;
1911 if core::mem::replace(&mut used[idx], true) {
1912 return Err(ParseError::DuplicateInputPad(spec.name.clone()));
1913 }
1914 mux_pad_of_link[k] = Some(idx as u8);
1915 }
1916 for &k in &incoming {
1917 if link_dest_req[k].is_none() {
1918 let idx = used
1919 .iter()
1920 .position(|u| !u)
1921 .expect("in_deg matches link count");
1922 used[idx] = true;
1923 mux_pad_of_link[k] = Some(idx as u8);
1924 }
1925 }
1926 if out_deg[ei] == 0 {
1927 // Nothing downstream: legal only for a terminal fan-in session
1928 // (M713), whose element consumes its inputs with no merged
1929 // output. A merging muxer here would silently drop its output.
1930 if !mux.is_terminal() {
1931 return Err(ParseError::MuxerWithoutOutput(spec.name.clone()));
1932 }
1933 graph
1934 .add_fanin_sink(GraphNodeRef::Muxer(mux), in_deg[ei] as u8)
1935 .node()
1936 } else {
1937 graph
1938 .add_muxer(GraphNodeRef::Muxer(mux), in_deg[ei] as u8)
1939 .node()
1940 }
1941 } else if is_select[ei] {
1942 // M476: a demux-select hook already built the multi-output demuxer
1943 // (probing the upstream file); splice it in with one port per pad.
1944 let demux = demux_select_node[ei]
1945 .take()
1946 .expect("select demux built above");
1947 graph
1948 .add_demux(GraphNodeRef::Demux(demux), out_deg[ei] as u8)
1949 .node()
1950 } else if is_demux(ei) {
1951 let mut demux = registry
1952 .make_demux(&spec.name, out_deg[ei])
1953 .ok_or_else(|| ParseError::UnknownElement(spec.name.clone()))?;
1954 apply_props(&mut demux, &spec.name, &spec.props)?;
1955 graph
1956 .add_demux(GraphNodeRef::Demux(demux), out_deg[ei] as u8)
1957 .node()
1958 } else if out_deg[ei] == 0 {
1959 let mut el = registry
1960 .make_element(&spec.name)
1961 .ok_or_else(|| ParseError::UnknownElement(spec.name.clone()))?;
1962 apply_props(&mut el, &spec.name, &spec.props)?;
1963 graph.add_sink(GraphNodeRef::Element(el))
1964 } else {
1965 let mut el = registry
1966 .make_element(&spec.name)
1967 .ok_or_else(|| ParseError::UnknownElement(spec.name.clone()))?;
1968 apply_props(&mut el, &spec.name, &spec.props)?;
1969 graph.add_transform(GraphNodeRef::Element(el))
1970 };
1971 // M842: a `name=` is the element's instance name at run time, not just
1972 // the handle pad references resolve against.
1973 if let Some(inst) = &spec.instance {
1974 graph.set_node_name(node, inst.clone());
1975 }
1976 // M847: `log-category=` renames this instance's log category, which the
1977 // runner hands to the element before naming it.
1978 if let Some(cat) = &spec.log_category {
1979 graph.set_node_log_category(node, cat.clone());
1980 }
1981 node_of.push(Some(node));
1982 }
1983
1984 // Auto-tee (M473): for each fan-out node that is not itself a tee/demux, splice
1985 // an implicit `tee` onto its output. The node's single output pad feeds the
1986 // tee (a plain blocking link); the tee's `out_deg` pads feed the consumers,
1987 // which the edge loop below sources from the tee instead of the node.
1988 let mut implicit_tee: Vec<Option<NodeId>> = alloc::vec![None; specs.len()];
1989 for ei in 0..specs.len() {
1990 if needs_tee(ei) {
1991 if let Some(src_node) = node_of[ei] {
1992 let tee = graph.add_tee(out_deg[ei] as u8).node();
1993 graph.link_with(PadId::from(src_node), PadId::from(tee), LinkPolicy::Block)?;
1994 implicit_tee[ei] = Some(tee);
1995 }
1996 }
1997 }
1998
1999 // Wire edges. Each tee or demux branch takes a distinct output pad (0..n) and
2000 // each muxer input a distinct input pad (0..n); every other output and input
2001 // is pad 0. A queue's `leaky=` rides along as the edge's `LinkPolicy`.
2002 let mut tee_next = alloc::vec![0u8; specs.len()];
2003 for (k, &(s, d, policy, capacity)) in links.iter().enumerate() {
2004 let node_s = node_of[s].expect("contracted link source is a real node");
2005 let node_d = node_of[d].expect("contracted link destination is a real node");
2006 let src = if let Some(tee) = implicit_tee[s] {
2007 // The fan-out node's consumers source from its spliced-in tee's pads.
2008 let index = tee_next[s];
2009 tee_next[s] += 1;
2010 PadId { node: tee, index }
2011 } else if is_tee(s) || is_demux(s) || is_select[s] || registry.is_fanout_src(&specs[s].name)
2012 {
2013 let index = tee_next[s];
2014 tee_next[s] += 1;
2015 PadId {
2016 node: node_s,
2017 index,
2018 }
2019 } else {
2020 PadId::from(node_s)
2021 };
2022 let dst = if is_muxer(d) {
2023 // The input index was resolved at construction (named pads via the
2024 // muxer's scheme, bare refs sequentially); every muxer link has one.
2025 let index = mux_pad_of_link[k].expect("muxer link assigned an input pad");
2026 PadId {
2027 node: node_d,
2028 index,
2029 }
2030 } else {
2031 PadId::from(node_d)
2032 };
2033 // `decodebin` fan-out (M482): splice a decoder between the demux port and the
2034 // branch consumer instead of a bare link, so each `d.video_0` / `d.audio_0`
2035 // branch receives DECODED (raw) frames. A text port carries `Text{Utf8}`
2036 // already, so it links straight through (no codec).
2037 if let Some(caps) = &decode_fanout_caps[s] {
2038 let port = src.index as usize;
2039 let kind = demux_pads[s]
2040 .get(port)
2041 .map(|r| r.kind)
2042 .unwrap_or(PadKind::Any);
2043 if !matches!(kind, PadKind::Text) {
2044 let target: &dyn Fn(&Caps) -> bool = match kind {
2045 PadKind::Audio => &is_raw_audio,
2046 _ => &is_raw_video,
2047 };
2048 registry
2049 .decodebin(
2050 &mut graph,
2051 src,
2052 dst,
2053 &caps[port],
2054 target,
2055 DECODEBIN_MAX_DEPTH,
2056 )
2057 .map_err(|_| ParseError::NoDecodeChain(alloc::format!("{:?}", caps[port])))?;
2058 continue;
2059 }
2060 }
2061 graph.link_full(src, dst, policy, capacity)?;
2062 }
2063
2064 Ok(graph)
2065}
2066
2067/// Map a `queue` / `queue2` node's `leaky=` property to the edge backpressure
2068/// policy it stands in for (M190). gst accepts the enum by value or nick:
2069/// `0`/`no` (lossless, the default), `1`/`upstream` (drop the newest incoming
2070/// buffer), `2`/`downstream` (drop the oldest queued buffer). The other buffering
2071/// bounds (`max-size-bytes` / `max-size-time`, `min-threshold-*`, `silent`) are
2072/// accepted but not modeled (g2g's link depth is a buffer count), so they are
2073/// ignored for paste compatibility rather than rejected; `max-size-buffers` maps
2074/// to the edge depth, see [`queue_capacity_of`].
2075fn queue_leaky_policy(spec: &ElementSpec) -> LinkPolicy {
2076 for (k, v) in &spec.props {
2077 if k == "leaky" {
2078 return match v.as_str() {
2079 "1" | "upstream" => LinkPolicy::DropNewest,
2080 "2" | "downstream" => LinkPolicy::DropOldest,
2081 // "0" / "no" and anything unrecognized: lossless block.
2082 _ => LinkPolicy::Block,
2083 };
2084 }
2085 }
2086 LinkPolicy::Block
2087}
2088
2089/// A `queue max-size-buffers=N` sets the depth of the edge it contracts to (the
2090/// gst per-queue buffer bound), overriding the runner's graph-wide `link_capacity`
2091/// for just that link. `0` in gst means "unbounded"; g2g has no unbounded channel,
2092/// so a `0` is ignored (falls back to the default depth). An unparseable value is
2093/// ignored too (paste compatibility).
2094fn queue_capacity_of(spec: &ElementSpec) -> Option<usize> {
2095 spec.props
2096 .iter()
2097 .find(|(k, _)| k == "max-size-buffers")
2098 .and_then(|(_, v)| v.parse::<usize>().ok())
2099 .filter(|&n| n > 0)
2100}
2101
2102/// Parse a `gst-launch`-style pipeline string into a runnable [`Graph`], building
2103/// each element by name from `registry`, applying its `key=value` properties, and
2104/// linking the chains (including `tee` branches) into the DAG. Roles follow
2105/// connectivity. The result drops straight onto
2106/// [`run_graph`](crate::runtime::run_graph).
2107///
2108/// ```text
2109/// videotestsrc num-buffers=3 ! tee name=t ! fakesink t. ! videoflip ! fakesink
2110/// ```
2111pub fn parse_launch(registry: &Registry, pipeline: &str) -> Result<Graph<GraphNode>, ParseError> {
2112 parse_launch_avoiding(registry, pipeline, &[])
2113}
2114
2115/// [`parse_launch`], with the auto-plug search forbidden from picking any
2116/// factory named in `avoided` (M1023). The line is otherwise parsed identically,
2117/// so an element the text names explicitly is still built: this bounds only what
2118/// a `decodebin` / `uridecodebin` / `playbin` may choose. An application retries
2119/// through here after a chosen decoder turned out not to decode the stream.
2120pub fn parse_launch_avoiding(
2121 registry: &Registry,
2122 pipeline: &str,
2123 avoided: &[&str],
2124) -> Result<Graph<GraphNode>, ParseError> {
2125 // The parser's own built-ins are not registry factories, but they are
2126 // element names all the same, so a chain may start on one.
2127 let knows = |name: &str| {
2128 registry.knows_element(name)
2129 || matches!(
2130 name,
2131 "queue"
2132 | "queue2"
2133 | "decodebin"
2134 | "uridecodebin"
2135 | "playbin"
2136 | "encodebin"
2137 | "encodebin2"
2138 | "transcodebin"
2139 )
2140 };
2141 let chains = parse_chains_with(pipeline, &knows)?;
2142 // playbin uri=X auto-fan-out (M382): a lone `playbin uri=` probes the
2143 // container via the registered hook and auto-builds source -> demux ->
2144 // per-stream decode -> auto sinks (multi-stream). Without a hook (or if it
2145 // declines, e.g. a non-Matroska file), fall through to build_graph, which
2146 // expands `playbin` to the single-stream pipeline (M196).
2147 if let Some(uri) = lone_playbin_uri(&chains) {
2148 // Try each registered hook (one per container type) until one handles the
2149 // URI; a hook returns Ok(None) to decline a container it does not parse.
2150 for hook in registry.playbin_hooks() {
2151 if let Some(graph) = hook(registry, uri)? {
2152 return Ok(splice_domain_converters(registry, graph));
2153 }
2154 }
2155 }
2156 Ok(splice_domain_converters(
2157 registry,
2158 build_graph(registry, chains, avoided)?,
2159 ))
2160}
2161
2162/// The factory to bar from the auto-plug search after a run ended in
2163/// `failed_element`, or `None` when re-plugging cannot help (M1023). Pass the
2164/// result to [`parse_launch_avoiding`] (accumulating it into `avoided`) and run
2165/// the line again; `None` means report the failure instead.
2166///
2167/// The caller owns the rest of the policy, because only it knows whether a
2168/// restart is safe: retry only while nothing has been presented (a run that
2169/// output nothing restarts invisibly, one that did not would repeat itself) and
2170/// not for a live source ([`has_live_source`], where a restart reopens the
2171/// connection and drops what was in flight).
2172///
2173/// `None` when the failing element is unknown to the registry, when the line
2174/// names it explicitly (the user asked for that element by name, so substituting
2175/// another would defy them), or when it has already been barred once.
2176pub fn fallback_factory(
2177 registry: &Registry,
2178 pipeline: &str,
2179 failed_element: &str,
2180 avoided: &[&str],
2181) -> Option<&'static str> {
2182 let factory = registry.factory_of_instance(failed_element)?;
2183 if avoided.contains(&factory) {
2184 return None;
2185 }
2186 let named_in_line = tokenize(pipeline).iter().any(|t| t == factory);
2187 (!named_in_line).then_some(factory)
2188}
2189
2190/// Whether any element in `graph` offers a live-source clock: a capture or
2191/// network source, which a restart would reopen at the cost of whatever was in
2192/// flight. The signal a caller checks before re-running a pipeline it has already
2193/// started (see [`fallback_factory`]).
2194pub fn has_live_source(graph: &Graph<GraphNode>) -> bool {
2195 (0..graph.node_count())
2196 .filter_map(|i| graph.element(NodeId(i as u32)))
2197 .filter_map(|node| match node {
2198 GraphNodeRef::Source(source) => source.provide_clock(),
2199 GraphNodeRef::Element(element) => element.provide_clock(),
2200 _ => None,
2201 })
2202 .any(|candidate| candidate.priority == crate::clock::ClockPriority::LiveSource)
2203}
2204
2205/// Splice memory-domain converters into a just-parsed graph (M1017), where the
2206/// registry carries a factory: a text pipeline that links a GPU decoder to a sink
2207/// in another domain gets the bridge (or the download) it needs without naming
2208/// it. A no-op when the domains already agree, or when no factory is registered.
2209fn splice_domain_converters(registry: &Registry, graph: Graph<GraphNode>) -> Graph<GraphNode> {
2210 match registry.domain_converter() {
2211 Some(factory) => crate::runtime::auto_plug_domain_converters(graph, &factory),
2212 None => graph,
2213 }
2214}
2215
2216/// The `uri=` of a pipeline that is a single bare `playbin uri=X` element (and
2217/// nothing else), the M382 multi-stream auto-fan-out trigger. `None` for any
2218/// other shape: a `playbin` mid-pipeline, alongside other elements, or without a
2219/// `uri=` is left to the normal builder (the M196 single-stream expansion).
2220fn lone_playbin_uri(chains: &[Chain]) -> Option<&str> {
2221 let [chain] = chains else { return None };
2222 let [Item::Element(spec)] = chain.as_slice() else {
2223 return None;
2224 };
2225 if spec.name != "playbin" {
2226 return None;
2227 }
2228 prop(spec, "uri")
2229}
2230
2231#[cfg(test)]
2232mod tests {
2233 use super::*;
2234
2235 fn item_names(chain: &Chain) -> Vec<&str> {
2236 chain
2237 .iter()
2238 .map(|i| match i {
2239 Item::Element(s) => s.name.as_str(),
2240 Item::Ref { name, .. } => name.as_str(),
2241 Item::Prebuilt(_) => "(prebuilt)",
2242 })
2243 .collect()
2244 }
2245
2246 #[test]
2247 fn parse_chains_splits_names_and_props() {
2248 let chains = parse_chains(
2249 "videotestsrc num-buffers=3 pattern=snow ! videoflip method=rotate-180 ! fakesink",
2250 )
2251 .unwrap();
2252 assert_eq!(chains.len(), 1);
2253 assert_eq!(
2254 item_names(&chains[0]),
2255 ["videotestsrc", "videoflip", "fakesink"]
2256 );
2257 let Item::Element(src) = &chains[0][0] else {
2258 panic!("first is an element")
2259 };
2260 assert_eq!(
2261 src.props,
2262 [
2263 ("num-buffers".to_string(), "3".to_string()),
2264 ("pattern".into(), "snow".into())
2265 ]
2266 );
2267 let Item::Element(sink) = &chains[0][2] else {
2268 panic!("last is an element")
2269 };
2270 assert!(sink.props.is_empty());
2271 }
2272
2273 #[test]
2274 fn parse_chains_strips_quoted_values() {
2275 // A double-quoted value has its quotes stripped.
2276 let chains = parse_chains("filesrc location=\"file.mp4\" ! fakesink").unwrap();
2277 let Item::Element(src) = &chains[0][0] else {
2278 panic!("element")
2279 };
2280 assert_eq!(
2281 src.props[0],
2282 ("location".to_string(), "file.mp4".to_string())
2283 );
2284 }
2285
2286 #[test]
2287 fn parse_chains_keeps_spaces_in_quoted_values() {
2288 // The quote-aware tokenizer keeps spaces inside a value, so a nested
2289 // element description (the `gstwrap` case) survives as one property.
2290 let chains = parse_chains("gstwrap element=\"x264enc bitrate=4000\" ! fakesink").unwrap();
2291 assert_eq!(item_names(&chains[0]), ["gstwrap", "fakesink"]);
2292 let Item::Element(w) = &chains[0][0] else {
2293 panic!("element")
2294 };
2295 assert_eq!(
2296 w.props[0],
2297 ("element".to_string(), "x264enc bitrate=4000".to_string())
2298 );
2299 }
2300
2301 #[test]
2302 fn parse_chains_keeps_bang_inside_quotes() {
2303 // A `!` inside a quoted value is literal, not a stage separator: one
2304 // element with one property, not two chained nodes.
2305 let chains = parse_chains("gstwrap element=\"a ! b\" ! fakesink").unwrap();
2306 assert_eq!(item_names(&chains[0]), ["gstwrap", "fakesink"]);
2307 let Item::Element(w) = &chains[0][0] else {
2308 panic!("element")
2309 };
2310 assert_eq!(w.props[0], ("element".to_string(), "a ! b".to_string()));
2311 }
2312
2313 #[test]
2314 fn tokenize_treats_quoted_region_as_one_token() {
2315 assert_eq!(
2316 tokenize("gstwrap element=\"x y\" ! sink"),
2317 ["gstwrap", "element=\"x y\"", "!", "sink"]
2318 );
2319 }
2320
2321 #[test]
2322 fn tokenize_treats_single_quoted_region_as_one_token() {
2323 assert_eq!(
2324 tokenize("filesink location='/my file.ts' ! sink"),
2325 ["filesink", "location='/my file.ts'", "!", "sink"]
2326 );
2327 }
2328
2329 #[test]
2330 fn single_quoted_value_is_unquoted() {
2331 let chains = parse_chains("videotestsrc ! identity note='a b c' ! fakesink").unwrap();
2332 let Item::Element(id) = &chains[0][1] else {
2333 panic!("element")
2334 };
2335 assert_eq!(id.props, [("note".to_string(), "a b c".to_string())]);
2336 }
2337
2338 #[test]
2339 fn escaped_space_keeps_a_value_whole() {
2340 // gst-launch 1.26 does the same: `\ ` is a literal space, not a token
2341 // break (verified against `filesink location=`).
2342 assert_eq!(
2343 tokenize(r"filesink location=/my\ file.ts ! sink"),
2344 ["filesink", r"location=/my\ file.ts", "!", "sink"]
2345 );
2346 let chains = parse_chains(r"videotestsrc ! filesink location=/my\ file.ts").unwrap();
2347 let Item::Element(fs) = &chains[0][1] else {
2348 panic!("element")
2349 };
2350 assert_eq!(fs.props, [("location".to_string(), "/my file.ts".into())]);
2351 }
2352
2353 #[test]
2354 fn escaped_quote_does_not_close_the_region() {
2355 // `"a\"b"` is one token holding `a"b`, as in gst-launch.
2356 assert_eq!(
2357 tokenize(r#"identity note="a\" b" ! sink"#),
2358 ["identity", r#"note="a\" b""#, "!", "sink"]
2359 );
2360 let chains = parse_chains(r#"videotestsrc ! identity note="a\" b""#).unwrap();
2361 let Item::Element(id) = &chains[0][1] else {
2362 panic!("element")
2363 };
2364 assert_eq!(id.props, [("note".to_string(), "a\" b".into())]);
2365 }
2366
2367 #[test]
2368 fn quoted_region_may_sit_mid_value() {
2369 // gst-launch rejects this ("no element ..."): its lexer only quotes a
2370 // whole value. g2g accepts the superset, so a path with one spaced
2371 // component needs no quoting of the rest.
2372 let chains =
2373 parse_chains(r#"videotestsrc ! filesink location=/tmp/"my dir"/a.ts"#).unwrap();
2374 let Item::Element(fs) = &chains[0][1] else {
2375 panic!("element")
2376 };
2377 assert_eq!(
2378 fs.props,
2379 [("location".to_string(), "/tmp/my dir/a.ts".into())]
2380 );
2381 }
2382
2383 #[test]
2384 fn unescaped_backslash_stays_literal() {
2385 // A Windows path survives unescaped. gst-launch would drop these
2386 // backslashes (`videos\a.ts` opens a file called `videosa.ts`), so this
2387 // is a deliberate deviation: `\` escapes only the launch-special
2388 // characters.
2389 assert_eq!(unquote_value(r"C:\videos\a.ts"), r"C:\videos\a.ts");
2390 assert_eq!(unquote_value(r"a\ b"), "a b");
2391 assert_eq!(unquote_value(r"a\\b"), r"a\b");
2392 }
2393
2394 #[test]
2395 fn hash_starts_a_comment_to_end_of_line() {
2396 // Trailing comment on one line, and a comment mid-pipeline across lines.
2397 let chains = parse_chains("videotestsrc ! fakesink # trailing note").unwrap();
2398 assert_eq!(item_names(&chains[0]), ["videotestsrc", "fakesink"]);
2399 let chains = parse_chains("videotestsrc # the source\n ! fakesink").unwrap();
2400 assert_eq!(item_names(&chains[0]), ["videotestsrc", "fakesink"]);
2401 }
2402
2403 #[test]
2404 fn hash_inside_a_value_is_literal_not_a_comment() {
2405 // A URI fragment (`#closed-captions=cc1`, `#t=10`) must survive; `#` is a
2406 // comment only at a token boundary.
2407 assert_eq!(
2408 tokenize("uridecodebin uri=file:///v.mp4#closed-captions=cc1 ! sink"),
2409 [
2410 "uridecodebin",
2411 "uri=file:///v.mp4#closed-captions=cc1",
2412 "!",
2413 "sink"
2414 ]
2415 );
2416 }
2417
2418 #[test]
2419 fn caps_range_value_error_carries_a_syntax_hint() {
2420 let e = ParseError::BadValue {
2421 element: "capsfilter".to_string(),
2422 key: "caps".to_string(),
2423 value: "video/x-raw,width=[1,1920]".to_string(),
2424 };
2425 let msg = alloc::format!("{e}");
2426 assert!(msg.contains("ranges"), "caps range hint present: {msg}");
2427 // A plain element property error keeps the bare message (no caps hint).
2428 let plain = ParseError::BadValue {
2429 element: "videobox".to_string(),
2430 key: "top".to_string(),
2431 value: "abc".to_string(),
2432 };
2433 assert!(!alloc::format!("{plain}").contains("ranges"));
2434 }
2435
2436 #[test]
2437 fn enum_value_error_lists_the_valid_nicks() {
2438 let e = ParseError::BadEnumValue {
2439 element: "videoflip".to_string(),
2440 key: "method".to_string(),
2441 value: "sideways".to_string(),
2442 values: "none | clockwise",
2443 };
2444 let msg = alloc::format!("{e}");
2445 assert!(msg.contains("videoflip"), "{msg}");
2446 assert!(msg.contains("'method'"), "{msg}");
2447 assert!(msg.contains("valid: none | clockwise"), "{msg}");
2448 assert!(!msg.contains("flag set"), "no flag hint for a plain enum");
2449 // A malformed flag set (the whole value is reported) also hints at the
2450 // `+` syntax.
2451 let flags = ParseError::BadEnumValue {
2452 element: "rtspsrc".to_string(),
2453 key: "protocols".to_string(),
2454 value: "udp+".to_string(),
2455 values: "udp | tcp",
2456 };
2457 assert!(alloc::format!("{flags}").contains("flag set"));
2458 }
2459
2460 #[test]
2461 fn caps_description_becomes_capsfilter() {
2462 // A bare `media/type,...` node is the inline caps-filter shorthand.
2463 let chains =
2464 parse_chains("videotestsrc ! video/x-raw,format=nv12,width=320 ! fakesink").unwrap();
2465 assert_eq!(
2466 item_names(&chains[0]),
2467 ["videotestsrc", "capsfilter", "fakesink"]
2468 );
2469 let Item::Element(caps) = &chains[0][1] else {
2470 panic!("element")
2471 };
2472 assert_eq!(
2473 caps.props,
2474 [(
2475 "caps".to_string(),
2476 "video/x-raw,format=nv12,width=320".to_string()
2477 )]
2478 );
2479 }
2480
2481 #[test]
2482 fn tee_branch_parses_into_two_chains() {
2483 // `name=` is the instance handle (not a property); `t.` opens the branch.
2484 let chains =
2485 parse_chains("videotestsrc ! tee name=t ! fakesink t. ! videoflip ! fakesink").unwrap();
2486 assert_eq!(chains.len(), 2);
2487 assert_eq!(item_names(&chains[0]), ["videotestsrc", "tee", "fakesink"]);
2488 assert_eq!(item_names(&chains[1]), ["t", "videoflip", "fakesink"]);
2489 let Item::Element(tee) = &chains[0][1] else {
2490 panic!("element")
2491 };
2492 assert_eq!(tee.instance.as_deref(), Some("t"));
2493 assert!(tee.props.is_empty(), "name= is the handle, not a property");
2494 assert!(matches!(&chains[1][0], Item::Ref { name, .. } if name == "t"));
2495 }
2496
2497 #[test]
2498 fn empty_and_too_few_stages_error() {
2499 let reg = Registry::new();
2500 assert!(matches!(parse_launch(®, " "), Err(ParseError::Empty)));
2501 assert!(matches!(
2502 parse_launch(®, "videotestsrc"),
2503 Err(ParseError::TooFewStages)
2504 ));
2505 }
2506
2507 #[test]
2508 fn malformed_property_is_reported() {
2509 assert!(matches!(
2510 parse_chains("videotestsrc bogus ! fakesink"),
2511 Err(ParseError::MalformedProperty { .. })
2512 ));
2513 }
2514
2515 /// gst-launch runs several top-level chains in one pipeline
2516 /// (`videotestsrc ! xvimagesink audiotestsrc ! pulsesink`); a bare element
2517 /// name after a completed chain opens the next one. Before, the sink ate it
2518 /// as a property and reported it malformed.
2519 #[test]
2520 fn a_second_chain_may_follow_a_sink() {
2521 let known = |n: &str| matches!(n, "videotestsrc" | "fakesink" | "filesink");
2522 let chains = parse_chains_with("videotestsrc ! fakesink videotestsrc ! fakesink", &known)
2523 .expect("two chains parse");
2524 let shapes: Vec<Vec<&str>> = chains.iter().map(item_names).collect();
2525 assert_eq!(
2526 shapes,
2527 [["videotestsrc", "fakesink"], ["videotestsrc", "fakesink"]],
2528 "one chain per source"
2529 );
2530 }
2531
2532 /// The property case must not regress: a bare token naming nothing
2533 /// registered is still the typo it looks like, not a new chain.
2534 #[test]
2535 fn a_bare_token_that_names_nothing_is_still_a_malformed_property() {
2536 let known = |n: &str| matches!(n, "videotestsrc" | "fakesink" | "filesink");
2537 assert!(matches!(
2538 parse_chains_with("videotestsrc bogus ! fakesink", &known),
2539 Err(ParseError::MalformedProperty { .. })
2540 ));
2541 // And after a sink, where the new-chain rule applies.
2542 assert!(matches!(
2543 parse_chains_with("videotestsrc ! fakesink bogus", &known),
2544 Err(ParseError::MalformedProperty { .. })
2545 ));
2546 }
2547
2548 /// A property whose value happens to name an element is still a property:
2549 /// the `=` decides.
2550 #[test]
2551 fn a_key_value_token_is_never_a_chain_head() {
2552 let known = |n: &str| matches!(n, "videotestsrc" | "fakesink" | "filesink");
2553 let chains =
2554 parse_chains_with("videotestsrc ! filesink location=fakesink", &known).expect("parses");
2555 let shapes: Vec<Vec<&str>> = chains.iter().map(item_names).collect();
2556 assert_eq!(shapes, [["videotestsrc", "filesink"]], "one chain");
2557 }
2558
2559 #[test]
2560 fn unknown_reference_is_reported() {
2561 // The degree / reference checks precede registry construction, so an
2562 // empty registry still surfaces them.
2563 let reg = Registry::new();
2564 let err = parse_launch(
2565 ®,
2566 "videotestsrc ! tee name=t ! fakesink nope. ! fakesink",
2567 )
2568 .unwrap_err();
2569 assert_eq!(err, ParseError::UnknownReference("nope".to_string()));
2570 }
2571
2572 #[test]
2573 fn duplicate_name_is_reported() {
2574 let reg = Registry::new();
2575 let err =
2576 parse_launch(®, "videotestsrc name=x ! videoflip name=x ! fakesink").unwrap_err();
2577 assert_eq!(err, ParseError::DuplicateName("x".to_string()));
2578 }
2579
2580 // Auto-tee (M473): fan-out without an explicit `tee` no longer errors; the
2581 // parser splices one in. Covered end to end (build + run + topology) in
2582 // g2g-plugins/tests/m118_launch_branching.rs, where real elements exist.
2583}