oxideav_core/registry/source.rs
1//! Generic source registry.
2//!
3//! `SourceRegistry` maps URI schemes (`file`, `http`, `rtmp`, `generate`,
4//! …) to opener functions and dispatches `open(uri)` to the right driver.
5//! A driver opens a URI as one of four shapes:
6//!
7//! * [`BytesSource`] — a `Read + Seek` byte stream that downstream code
8//! then passes to a container demuxer (the historical shape, used by
9//! `file://` and `http(s)://`).
10//! * [`PacketSource`] — a producer of already-demuxed [`Packet`]s. Used
11//! by transport-layer protocols that do their own demux (RTMP, future
12//! SRT / WebRTC). Skips the container layer entirely.
13//! * [`FrameSource`] — a producer of already-decoded [`Frame`]s. Used by
14//! synthetic generators that emit frames natively, skipping both the
15//! container and decoder stages.
16//! * [`MultiTitleSource`] — a source that emits N discrete byte streams
17//! (titles), one per logical "segment" the source carries
18//! (BD-ROM chapters or unique titles, DVD VTS entries, multi-title
19//! MKV editions, …). The CLI's `oxideav remux` substitutes a
20//! per-title token into a `%s.<ext>`-style output-path template, so
21//! each title lands in its own output file.
22//!
23//! The driver picks the variant when it registers; [`SourceRegistry::open`]
24//! returns the corresponding [`SourceOutput`] enum so the pipeline
25//! executor can branch on the source shape.
26
27use std::collections::HashMap;
28use std::io::{Read, Seek};
29
30use crate::{CodecParameters, Error, Frame, Packet, Result, StreamInfo};
31
32// ───────────────────────── traits ─────────────────────────
33
34/// A seekable byte stream (`Read + Seek + Send`). Replaces the historical
35/// `Box<dyn ReadSeek>` opener-return type with a name that mirrors the
36/// other source-shape traits in this module. Blanket-implemented for
37/// every type that satisfies the bounds, so existing readers (files,
38/// `Cursor<Vec<u8>>`, HTTP-over-Range adapters) work unchanged.
39pub trait BytesSource: Read + Seek + Send {}
40impl<T: Read + Seek + Send> BytesSource for T {}
41
42/// A producer of already-demuxed [`Packet`]s.
43///
44/// Used by transport-layer protocols that perform demux themselves
45/// (RTMP, RTSP, …). The pipeline executor consumes packets directly,
46/// skipping the container-demux stage that bytes-shape sources go
47/// through.
48pub trait PacketSource: Send {
49 /// Streams advertised by this source. Stable across the lifetime of
50 /// the source.
51 fn streams(&self) -> &[StreamInfo];
52
53 /// Read the next packet from any stream. Returns [`Error::Eof`] at
54 /// end of stream.
55 fn next_packet(&mut self) -> Result<Packet>;
56
57 /// Source-level metadata as ordered (key, value) pairs. Default is
58 /// empty.
59 fn metadata(&self) -> &[(String, String)] {
60 &[]
61 }
62
63 /// Source-level duration in microseconds, if known. Default is
64 /// `None`. Live sources (RTMP push, etc.) typically return `None`.
65 fn duration_micros(&self) -> Option<i64> {
66 None
67 }
68}
69
70/// A source that emits N discrete byte streams ("titles") rather
71/// than a single contiguous one.
72///
73/// The motivating shape is BD-ROM: a disc contains many *titles*
74/// (whole movies, behind-the-scenes featurettes, trailers) and each
75/// title can be sliced further into *chapters*. The Blu-ray source
76/// driver expresses both shapes through this trait — a URI like
77/// `bluray:///path?title=1&chapters=2-5` opens a [`MultiTitleSource`]
78/// whose four titles are chapters 2, 3, 4, 5 of disc-title 1; a URI
79/// without `?chapters=` opens a [`MultiTitleSource`] with a single
80/// title (the autoplay title). DVD-Video, multi-edition MKV, and
81/// any other format with explicit segment structure plug in the
82/// same way.
83///
84/// Downstream callers fan out: each title is opened as its own
85/// [`BytesSource`], demuxed independently, and written to its own
86/// output path. The CLI's `oxideav remux` substitutes
87/// [`Self::title_label`] into a `%s` token in the output-path
88/// template so each title lands in a separate file. Other front-ends
89/// (`oxideplay bluray://`, a future GUI title-picker, …) can iterate
90/// titles the same way.
91///
92/// Sources that don't have multi-title structure should keep
93/// returning a [`BytesSource`] — there's no benefit to wrapping a
94/// single-title file in this trait.
95pub trait MultiTitleSource: Send {
96 /// Number of titles this source emits. Stable for the lifetime
97 /// of the source — title discovery happens at `open` time, not
98 /// while streaming.
99 fn title_count(&self) -> usize;
100
101 /// Open the title at `index` (0-based) as a single-stream
102 /// [`BytesSource`] the existing container registry can demux.
103 /// `index` must satisfy `index < self.title_count()`. Calling
104 /// `open_title` more than once on the same index is allowed —
105 /// the returned source is a fresh handle each time.
106 fn open_title(&mut self, index: usize) -> Result<Box<dyn BytesSource>>;
107
108 /// Stable per-title identifier substituted into a `%s` token of
109 /// a templated output path. Examples: `"3"` for chapter 3,
110 /// `"t01"` for title 1, `"introduction"` for a named edition.
111 /// Returned values must be filename-safe: ASCII letters / digits
112 /// / `-` / `_`, no path separators, no whitespace, no leading
113 /// dot. Calling code is free to additionally sanitise; an empty
114 /// string is rejected.
115 fn title_label(&self, index: usize) -> String;
116
117 /// Human-readable display name for the title (e.g.
118 /// `"Kite Uncut — Director's Cut"`) — used by interactive
119 /// front-ends to render menus. `None` when the source carries
120 /// no name. The default returns `None`.
121 fn title_display_name(&self, index: usize) -> Option<String> {
122 let _ = index;
123 None
124 }
125
126 /// Container-format hint for the title's byte stream
127 /// (`"mpegts"`, `"matroska"`, `"mp4"`, …). When `Some`, callers
128 /// can skip the format-detector pass and hand the bytes straight
129 /// to that demuxer. `None` means "sniff it" — preserves the
130 /// existing detection path. The default returns `None`.
131 fn title_container_hint(&self, index: usize) -> Option<&'static str> {
132 let _ = index;
133 None
134 }
135
136 /// Source-level metadata as ordered (key, value) pairs (disc
137 /// label, BDMT `<di:name>`, region code, …). Default is empty.
138 fn metadata(&self) -> &[(String, String)] {
139 &[]
140 }
141}
142
143/// A producer of already-decoded [`Frame`]s.
144///
145/// Used by synthetic generators (testsrc, sine sweep, gradient image,
146/// …) that emit decoded frames natively. The pipeline executor consumes
147/// frames directly, skipping both the container-demux and decode stages.
148pub trait FrameSource: Send {
149 /// Codec parameters describing the frames this source emits. Stable
150 /// across the lifetime of the source. Even though the frames are
151 /// already decoded, downstream filters and encoders need the
152 /// parameter shape (sample rate / pixel format / channel layout /
153 /// frame rate / …) to configure themselves.
154 fn params(&self) -> &CodecParameters;
155
156 /// Produce the next frame. Returns [`Error::Eof`] at end of stream.
157 fn next_frame(&mut self) -> Result<Frame>;
158
159 /// Source-level metadata as ordered (key, value) pairs. Default is
160 /// empty.
161 fn metadata(&self) -> &[(String, String)] {
162 &[]
163 }
164
165 /// Source-level duration in microseconds, if known. Default is
166 /// `None`.
167 fn duration_micros(&self) -> Option<i64> {
168 None
169 }
170}
171
172/// What a [`SourceRegistry::open`] call returns. The variant is decided
173/// at driver-registration time, so callers can match on the shape and
174/// branch the pipeline accordingly.
175///
176/// **Marked `#[non_exhaustive]`** so a new source kind (e.g. a future
177/// `LiveStream` variant) can be added without semver-breaking
178/// downstream consumers. Match arms must include a wildcard.
179#[non_exhaustive]
180pub enum SourceOutput {
181 /// A raw byte stream — feed it to container probing / a demuxer.
182 Bytes(Box<dyn BytesSource>),
183 /// Already-demuxed compressed packets — feed them to decoders.
184 Packets(Box<dyn PacketSource>),
185 /// Already-decoded frames (e.g. a capture device) — feed them to
186 /// filters / encoders directly.
187 Frames(Box<dyn FrameSource>),
188 /// A multi-title source (BD-ROM, DVD-Video, multi-edition MKV).
189 /// Callers fan out: each title is opened independently via
190 /// [`MultiTitleSource::open_title`], demuxed, and routed to its
191 /// own output sink.
192 MultiTitle(Box<dyn MultiTitleSource>),
193}
194
195// ───────────────────────── opener function aliases ─────────────────────────
196
197/// Opener for a [`BytesSource`] driver.
198pub type OpenBytesFn = fn(uri: &str) -> Result<Box<dyn BytesSource>>;
199
200/// Opener for a [`PacketSource`] driver.
201pub type OpenPacketsFn = fn(uri: &str) -> Result<Box<dyn PacketSource>>;
202
203/// Opener for a [`FrameSource`] driver.
204pub type OpenFramesFn = fn(uri: &str) -> Result<Box<dyn FrameSource>>;
205
206/// Opener for a [`MultiTitleSource`] driver.
207pub type OpenMultiTitleFn = fn(uri: &str) -> Result<Box<dyn MultiTitleSource>>;
208
209/// Internal per-scheme entry: which opener kind is registered for this
210/// scheme. Stored in a single map so [`SourceRegistry::open`] can
211/// dispatch with a single lookup, then match the variant to wrap in the
212/// returned [`SourceOutput`].
213enum OpenerEntry {
214 Bytes(OpenBytesFn),
215 Packets(OpenPacketsFn),
216 Frames(OpenFramesFn),
217 MultiTitle(OpenMultiTitleFn),
218}
219
220// ───────────────────────── SourceRegistry ─────────────────────────
221
222/// Registry mapping URI schemes to opener functions. Each scheme picks
223/// one of three opener kinds (bytes / packets / frames) at registration
224/// time; callers see the choice via the [`SourceOutput`] variant
225/// returned from [`open`](Self::open).
226#[derive(Default)]
227pub struct SourceRegistry {
228 schemes: HashMap<String, OpenerEntry>,
229 /// Schemes in first-registration order — drives
230 /// [`schemes`](Self::schemes) so listings are stable across
231 /// processes.
232 order: Vec<String>,
233}
234
235impl SourceRegistry {
236 /// Empty registry. Callers must register at least one driver before
237 /// calling [`open`](Self::open). The conventional minimum is the
238 /// `file` driver (provided by the `oxideav-source` crate).
239 pub fn new() -> Self {
240 Self::default()
241 }
242
243 /// Register a [`BytesSource`] opener for a scheme. Schemes are
244 /// normalised to ASCII lowercase. Replaces any prior registration
245 /// (including registrations of other opener kinds).
246 pub fn register_bytes(&mut self, scheme: &str, opener: OpenBytesFn) {
247 self.insert(scheme, OpenerEntry::Bytes(opener));
248 }
249
250 /// Shared insert: lowercase the scheme, remember first-seen order,
251 /// replace any prior entry.
252 fn insert(&mut self, scheme: &str, entry: OpenerEntry) {
253 let key = scheme.to_ascii_lowercase();
254 if self.schemes.insert(key.clone(), entry).is_none() {
255 self.order.push(key);
256 }
257 }
258
259 /// Register a [`PacketSource`] opener for a scheme. Schemes are
260 /// normalised to ASCII lowercase. Replaces any prior registration
261 /// (including registrations of other opener kinds).
262 pub fn register_packets(&mut self, scheme: &str, opener: OpenPacketsFn) {
263 self.insert(scheme, OpenerEntry::Packets(opener));
264 }
265
266 /// Register a [`FrameSource`] opener for a scheme. Schemes are
267 /// normalised to ASCII lowercase. Replaces any prior registration
268 /// (including registrations of other opener kinds).
269 pub fn register_frames(&mut self, scheme: &str, opener: OpenFramesFn) {
270 self.insert(scheme, OpenerEntry::Frames(opener));
271 }
272
273 /// Register a [`MultiTitleSource`] opener for a scheme. Schemes
274 /// are normalised to ASCII lowercase. Replaces any prior
275 /// registration (including registrations of other opener kinds).
276 pub fn register_multi_title(&mut self, scheme: &str, opener: OpenMultiTitleFn) {
277 self.insert(scheme, OpenerEntry::MultiTitle(opener));
278 }
279
280 /// Open a URI. The URI's scheme determines which opener runs; bare
281 /// paths (no scheme) and unrecognised schemes both fall back to the
282 /// `file` driver if it is registered.
283 ///
284 /// Returns a [`SourceOutput`] whose variant matches the registered
285 /// opener kind: bytes-shape drivers return `SourceOutput::Bytes`,
286 /// packet-shape drivers return `SourceOutput::Packets`, and so on.
287 pub fn open(&self, uri_str: &str) -> Result<SourceOutput> {
288 let (scheme, _) = split_scheme(uri_str);
289 let scheme = scheme.to_ascii_lowercase();
290 if let Some(entry) = self.schemes.get(&scheme) {
291 return dispatch(entry, uri_str);
292 }
293 // Fall back to file driver for unknown schemes.
294 if let Some(entry) = self.schemes.get("file") {
295 return dispatch(entry, uri_str);
296 }
297 Err(Error::Unsupported(format!(
298 "no source driver for scheme '{scheme}' (URI: {uri_str})"
299 )))
300 }
301
302 /// Iterate the registered schemes (for diagnostics) in first-
303 /// registration order; re-registering a scheme keeps its slot.
304 pub fn schemes(&self) -> impl Iterator<Item = &str> {
305 self.order.iter().map(|s| s.as_str())
306 }
307}
308
309fn dispatch(entry: &OpenerEntry, uri_str: &str) -> Result<SourceOutput> {
310 match entry {
311 OpenerEntry::Bytes(open) => open(uri_str).map(SourceOutput::Bytes),
312 OpenerEntry::Packets(open) => open(uri_str).map(SourceOutput::Packets),
313 OpenerEntry::Frames(open) => open(uri_str).map(SourceOutput::Frames),
314 OpenerEntry::MultiTitle(open) => open(uri_str).map(SourceOutput::MultiTitle),
315 }
316}
317
318/// Split a URI into `(scheme, rest)`. Bare paths (no scheme) report scheme
319/// `"file"` and `rest = uri`. Path-like inputs that happen to start with
320/// `c:` on Windows are treated as bare paths.
321pub(crate) fn split_scheme(uri: &str) -> (&str, &str) {
322 if let Some(idx) = uri.find(':') {
323 let (scheme, rest) = uri.split_at(idx);
324 let rest = &rest[1..]; // skip ':'
325
326 // Reject single-letter scheme that looks like a Windows drive letter.
327 if scheme.len() == 1 && scheme.chars().next().unwrap().is_ascii_alphabetic() {
328 return ("file", uri);
329 }
330
331 // Scheme must be ASCII alphanumeric / `+` / `-` / `.`, starting with a letter.
332 let valid = !scheme.is_empty()
333 && scheme.chars().next().unwrap().is_ascii_alphabetic()
334 && scheme
335 .chars()
336 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
337
338 if !valid {
339 return ("file", uri);
340 }
341
342 // Strip leading `//` from rest if present.
343 let rest = rest.strip_prefix("//").unwrap_or(rest);
344 return (scheme, rest);
345 }
346 ("file", uri)
347}
348
349// ───────────────────────── tests ─────────────────────────
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::frame::{AudioFrame, Frame};
355 use crate::packet::Packet;
356 use crate::stream::{CodecId, CodecParameters, StreamInfo};
357 use crate::time::TimeBase;
358 use std::io::{Cursor, Read};
359
360 // ---- mock BytesSource ----
361 fn open_bytes_mock(_uri: &str) -> Result<Box<dyn BytesSource>> {
362 Ok(Box::new(Cursor::new(b"hello world".to_vec())))
363 }
364
365 #[test]
366 fn register_bytes_and_open_returns_bytes_variant() {
367 let mut reg = SourceRegistry::new();
368 reg.register_bytes("mockb", open_bytes_mock);
369 let out = reg.open("mockb://anything").expect("open");
370 match out {
371 SourceOutput::Bytes(mut r) => {
372 let mut buf = String::new();
373 r.read_to_string(&mut buf).unwrap();
374 assert_eq!(buf, "hello world");
375 }
376 _ => panic!("expected SourceOutput::Bytes"),
377 }
378 }
379
380 // ---- mock PacketSource ----
381 struct MockPacketSource {
382 streams: Vec<StreamInfo>,
383 emitted: bool,
384 }
385
386 impl MockPacketSource {
387 fn new() -> Self {
388 let params = CodecParameters::audio(CodecId::new("pcm_s16le"));
389 let s = StreamInfo {
390 index: 0,
391 time_base: TimeBase::new(1, 1000),
392 duration: None,
393 start_time: None,
394 params,
395 };
396 Self {
397 streams: vec![s],
398 emitted: false,
399 }
400 }
401 }
402
403 impl PacketSource for MockPacketSource {
404 fn streams(&self) -> &[StreamInfo] {
405 &self.streams
406 }
407 fn next_packet(&mut self) -> Result<Packet> {
408 if self.emitted {
409 return Err(Error::Eof);
410 }
411 self.emitted = true;
412 Ok(Packet::new(0, TimeBase::new(1, 1000), vec![1, 2, 3, 4]))
413 }
414 }
415
416 fn open_packets_mock(_uri: &str) -> Result<Box<dyn PacketSource>> {
417 Ok(Box::new(MockPacketSource::new()))
418 }
419
420 #[test]
421 fn register_packets_and_open_returns_packets_variant() {
422 let mut reg = SourceRegistry::new();
423 reg.register_packets("mockp", open_packets_mock);
424 let out = reg.open("mockp://anything").expect("open");
425 match out {
426 SourceOutput::Packets(mut p) => {
427 assert_eq!(p.streams().len(), 1);
428 let pkt = p.next_packet().expect("first packet");
429 assert_eq!(pkt.data, vec![1, 2, 3, 4]);
430 assert!(matches!(p.next_packet(), Err(Error::Eof)));
431 }
432 _ => panic!("expected SourceOutput::Packets"),
433 }
434 }
435
436 // ---- mock FrameSource ----
437 struct MockFrameSource {
438 params: CodecParameters,
439 emitted: bool,
440 }
441
442 impl MockFrameSource {
443 fn new() -> Self {
444 Self {
445 params: CodecParameters::audio(CodecId::new("pcm_s16le")),
446 emitted: false,
447 }
448 }
449 }
450
451 impl FrameSource for MockFrameSource {
452 fn params(&self) -> &CodecParameters {
453 &self.params
454 }
455 fn next_frame(&mut self) -> Result<Frame> {
456 if self.emitted {
457 return Err(Error::Eof);
458 }
459 self.emitted = true;
460 Ok(Frame::Audio(AudioFrame {
461 samples: 1,
462 pts: Some(0),
463 data: vec![vec![0u8, 0u8]],
464 }))
465 }
466 }
467
468 fn open_frames_mock(_uri: &str) -> Result<Box<dyn FrameSource>> {
469 Ok(Box::new(MockFrameSource::new()))
470 }
471
472 #[test]
473 fn register_frames_and_open_returns_frames_variant() {
474 let mut reg = SourceRegistry::new();
475 reg.register_frames("mockf", open_frames_mock);
476 let out = reg.open("mockf://anything").expect("open");
477 match out {
478 SourceOutput::Frames(mut f) => {
479 assert_eq!(f.params().codec_id.as_str(), "pcm_s16le");
480 let frame = f.next_frame().expect("first frame");
481 match frame {
482 Frame::Audio(a) => assert_eq!(a.samples, 1),
483 _ => panic!("expected audio frame"),
484 }
485 assert!(matches!(f.next_frame(), Err(Error::Eof)));
486 }
487 _ => panic!("expected SourceOutput::Frames"),
488 }
489 }
490
491 #[test]
492 fn unknown_scheme_falls_back_to_file_when_registered() {
493 let mut reg = SourceRegistry::new();
494 reg.register_bytes("file", open_bytes_mock);
495 // No `foo` driver — falls through to the `file` driver.
496 let out = reg.open("foo://x").expect("fallback open");
497 assert!(matches!(out, SourceOutput::Bytes(_)));
498 }
499
500 #[test]
501 fn unknown_scheme_with_no_file_driver_errors() {
502 let reg = SourceRegistry::new();
503 let r = reg.open("nope://x");
504 assert!(matches!(r, Err(Error::Unsupported(_))));
505 }
506
507 // ---- mock MultiTitleSource ----
508 struct MockMultiTitleSource {
509 labels: Vec<String>,
510 }
511
512 impl MultiTitleSource for MockMultiTitleSource {
513 fn title_count(&self) -> usize {
514 self.labels.len()
515 }
516 fn open_title(&mut self, index: usize) -> Result<Box<dyn BytesSource>> {
517 if index >= self.labels.len() {
518 return Err(Error::Unsupported(format!(
519 "no title {index} (have {})",
520 self.labels.len()
521 )));
522 }
523 // Each title is just its label repeated 4×.
524 let payload = self.labels[index].as_bytes().repeat(4);
525 Ok(Box::new(Cursor::new(payload)))
526 }
527 fn title_label(&self, index: usize) -> String {
528 self.labels[index].clone()
529 }
530 fn title_display_name(&self, index: usize) -> Option<String> {
531 Some(format!("Title {}", self.labels[index]))
532 }
533 fn title_container_hint(&self, _index: usize) -> Option<&'static str> {
534 Some("mpegts")
535 }
536 }
537
538 fn open_multi_title_mock(_uri: &str) -> Result<Box<dyn MultiTitleSource>> {
539 Ok(Box::new(MockMultiTitleSource {
540 labels: vec!["1".to_string(), "2".to_string(), "3".to_string()],
541 }))
542 }
543
544 #[test]
545 fn register_multi_title_and_open_returns_multi_title_variant() {
546 let mut reg = SourceRegistry::new();
547 reg.register_multi_title("mockmt", open_multi_title_mock);
548 let out = reg.open("mockmt://anything").expect("open");
549 match out {
550 SourceOutput::MultiTitle(mut mt) => {
551 assert_eq!(mt.title_count(), 3);
552 assert_eq!(mt.title_label(0), "1");
553 assert_eq!(mt.title_display_name(2).as_deref(), Some("Title 3"));
554 assert_eq!(mt.title_container_hint(0), Some("mpegts"));
555 let mut buf = String::new();
556 mt.open_title(1)
557 .expect("title 1")
558 .read_to_string(&mut buf)
559 .unwrap();
560 assert_eq!(buf, "2222");
561 }
562 _ => panic!("expected SourceOutput::MultiTitle"),
563 }
564 }
565
566 #[test]
567 fn register_overrides_prior_kind() {
568 // Registering `mock` first as bytes then as frames should leave
569 // only the frames opener active (last write wins).
570 let mut reg = SourceRegistry::new();
571 reg.register_bytes("mock", open_bytes_mock);
572 reg.register_frames("mock", open_frames_mock);
573 let out = reg.open("mock://x").expect("open");
574 assert!(matches!(out, SourceOutput::Frames(_)));
575 }
576
577 #[test]
578 fn schemes_iterator_follows_first_registration_order() {
579 for _ in 0..200 {
580 let mut reg = SourceRegistry::new();
581 reg.register_bytes("zeta", open_bytes_mock);
582 reg.register_packets("Alpha", open_packets_mock);
583 reg.register_frames("omicron", open_frames_mock);
584 // Re-registering (different kind, different case) keeps
585 // the slot and the lowercase key.
586 reg.register_frames("ZETA", open_frames_mock);
587 assert_eq!(
588 reg.schemes().collect::<Vec<_>>(),
589 vec!["zeta", "alpha", "omicron"]
590 );
591 assert!(matches!(
592 reg.open("zeta://x").unwrap(),
593 SourceOutput::Frames(_)
594 ));
595 }
596 }
597
598 #[test]
599 fn schemes_iterator_lists_registered() {
600 let mut reg = SourceRegistry::new();
601 reg.register_bytes("mockb", open_bytes_mock);
602 reg.register_packets("mockp", open_packets_mock);
603 reg.register_frames("mockf", open_frames_mock);
604 let mut names: Vec<&str> = reg.schemes().collect();
605 names.sort();
606 assert_eq!(names, vec!["mockb", "mockf", "mockp"]);
607 }
608}