Skip to main content

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    Bytes(Box<dyn BytesSource>),
182    Packets(Box<dyn PacketSource>),
183    Frames(Box<dyn FrameSource>),
184    /// A multi-title source (BD-ROM, DVD-Video, multi-edition MKV).
185    /// Callers fan out: each title is opened independently via
186    /// [`MultiTitleSource::open_title`], demuxed, and routed to its
187    /// own output sink.
188    MultiTitle(Box<dyn MultiTitleSource>),
189}
190
191// ───────────────────────── opener function aliases ─────────────────────────
192
193/// Opener for a [`BytesSource`] driver.
194pub type OpenBytesFn = fn(uri: &str) -> Result<Box<dyn BytesSource>>;
195
196/// Opener for a [`PacketSource`] driver.
197pub type OpenPacketsFn = fn(uri: &str) -> Result<Box<dyn PacketSource>>;
198
199/// Opener for a [`FrameSource`] driver.
200pub type OpenFramesFn = fn(uri: &str) -> Result<Box<dyn FrameSource>>;
201
202/// Opener for a [`MultiTitleSource`] driver.
203pub type OpenMultiTitleFn = fn(uri: &str) -> Result<Box<dyn MultiTitleSource>>;
204
205/// Internal per-scheme entry: which opener kind is registered for this
206/// scheme. Stored in a single map so [`SourceRegistry::open`] can
207/// dispatch with a single lookup, then match the variant to wrap in the
208/// returned [`SourceOutput`].
209enum OpenerEntry {
210    Bytes(OpenBytesFn),
211    Packets(OpenPacketsFn),
212    Frames(OpenFramesFn),
213    MultiTitle(OpenMultiTitleFn),
214}
215
216// ───────────────────────── SourceRegistry ─────────────────────────
217
218/// Registry mapping URI schemes to opener functions. Each scheme picks
219/// one of three opener kinds (bytes / packets / frames) at registration
220/// time; callers see the choice via the [`SourceOutput`] variant
221/// returned from [`open`](Self::open).
222#[derive(Default)]
223pub struct SourceRegistry {
224    schemes: HashMap<String, OpenerEntry>,
225}
226
227impl SourceRegistry {
228    /// Empty registry. Callers must register at least one driver before
229    /// calling [`open`](Self::open). The conventional minimum is the
230    /// `file` driver (provided by the `oxideav-source` crate).
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Register a [`BytesSource`] opener for a scheme. Schemes are
236    /// normalised to ASCII lowercase. Replaces any prior registration
237    /// (including registrations of other opener kinds).
238    pub fn register_bytes(&mut self, scheme: &str, opener: OpenBytesFn) {
239        self.schemes
240            .insert(scheme.to_ascii_lowercase(), OpenerEntry::Bytes(opener));
241    }
242
243    /// Register a [`PacketSource`] 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_packets(&mut self, scheme: &str, opener: OpenPacketsFn) {
247        self.schemes
248            .insert(scheme.to_ascii_lowercase(), OpenerEntry::Packets(opener));
249    }
250
251    /// Register a [`FrameSource`] opener for a scheme. Schemes are
252    /// normalised to ASCII lowercase. Replaces any prior registration
253    /// (including registrations of other opener kinds).
254    pub fn register_frames(&mut self, scheme: &str, opener: OpenFramesFn) {
255        self.schemes
256            .insert(scheme.to_ascii_lowercase(), OpenerEntry::Frames(opener));
257    }
258
259    /// Register a [`MultiTitleSource`] opener for a scheme. Schemes
260    /// are normalised to ASCII lowercase. Replaces any prior
261    /// registration (including registrations of other opener kinds).
262    pub fn register_multi_title(&mut self, scheme: &str, opener: OpenMultiTitleFn) {
263        self.schemes
264            .insert(scheme.to_ascii_lowercase(), OpenerEntry::MultiTitle(opener));
265    }
266
267    /// Open a URI. The URI's scheme determines which opener runs; bare
268    /// paths (no scheme) and unrecognised schemes both fall back to the
269    /// `file` driver if it is registered.
270    ///
271    /// Returns a [`SourceOutput`] whose variant matches the registered
272    /// opener kind: bytes-shape drivers return `SourceOutput::Bytes`,
273    /// packet-shape drivers return `SourceOutput::Packets`, and so on.
274    pub fn open(&self, uri_str: &str) -> Result<SourceOutput> {
275        let (scheme, _) = split_scheme(uri_str);
276        let scheme = scheme.to_ascii_lowercase();
277        if let Some(entry) = self.schemes.get(&scheme) {
278            return dispatch(entry, uri_str);
279        }
280        // Fall back to file driver for unknown schemes.
281        if let Some(entry) = self.schemes.get("file") {
282            return dispatch(entry, uri_str);
283        }
284        Err(Error::Unsupported(format!(
285            "no source driver for scheme '{scheme}' (URI: {uri_str})"
286        )))
287    }
288
289    /// Iterate the registered schemes (for diagnostics).
290    pub fn schemes(&self) -> impl Iterator<Item = &str> {
291        self.schemes.keys().map(|s| s.as_str())
292    }
293}
294
295fn dispatch(entry: &OpenerEntry, uri_str: &str) -> Result<SourceOutput> {
296    match entry {
297        OpenerEntry::Bytes(open) => open(uri_str).map(SourceOutput::Bytes),
298        OpenerEntry::Packets(open) => open(uri_str).map(SourceOutput::Packets),
299        OpenerEntry::Frames(open) => open(uri_str).map(SourceOutput::Frames),
300        OpenerEntry::MultiTitle(open) => open(uri_str).map(SourceOutput::MultiTitle),
301    }
302}
303
304/// Split a URI into `(scheme, rest)`. Bare paths (no scheme) report scheme
305/// `"file"` and `rest = uri`. Path-like inputs that happen to start with
306/// `c:` on Windows are treated as bare paths.
307pub(crate) fn split_scheme(uri: &str) -> (&str, &str) {
308    if let Some(idx) = uri.find(':') {
309        let (scheme, rest) = uri.split_at(idx);
310        let rest = &rest[1..]; // skip ':'
311
312        // Reject single-letter scheme that looks like a Windows drive letter.
313        if scheme.len() == 1 && scheme.chars().next().unwrap().is_ascii_alphabetic() {
314            return ("file", uri);
315        }
316
317        // Scheme must be ASCII alphanumeric / `+` / `-` / `.`, starting with a letter.
318        let valid = !scheme.is_empty()
319            && scheme.chars().next().unwrap().is_ascii_alphabetic()
320            && scheme
321                .chars()
322                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
323
324        if !valid {
325            return ("file", uri);
326        }
327
328        // Strip leading `//` from rest if present.
329        let rest = rest.strip_prefix("//").unwrap_or(rest);
330        return (scheme, rest);
331    }
332    ("file", uri)
333}
334
335// ───────────────────────── tests ─────────────────────────
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::frame::{AudioFrame, Frame};
341    use crate::packet::Packet;
342    use crate::stream::{CodecId, CodecParameters, StreamInfo};
343    use crate::time::TimeBase;
344    use std::io::{Cursor, Read};
345
346    // ---- mock BytesSource ----
347    fn open_bytes_mock(_uri: &str) -> Result<Box<dyn BytesSource>> {
348        Ok(Box::new(Cursor::new(b"hello world".to_vec())))
349    }
350
351    #[test]
352    fn register_bytes_and_open_returns_bytes_variant() {
353        let mut reg = SourceRegistry::new();
354        reg.register_bytes("mockb", open_bytes_mock);
355        let out = reg.open("mockb://anything").expect("open");
356        match out {
357            SourceOutput::Bytes(mut r) => {
358                let mut buf = String::new();
359                r.read_to_string(&mut buf).unwrap();
360                assert_eq!(buf, "hello world");
361            }
362            _ => panic!("expected SourceOutput::Bytes"),
363        }
364    }
365
366    // ---- mock PacketSource ----
367    struct MockPacketSource {
368        streams: Vec<StreamInfo>,
369        emitted: bool,
370    }
371
372    impl MockPacketSource {
373        fn new() -> Self {
374            let params = CodecParameters::audio(CodecId::new("pcm_s16le"));
375            let s = StreamInfo {
376                index: 0,
377                time_base: TimeBase::new(1, 1000),
378                duration: None,
379                start_time: None,
380                params,
381            };
382            Self {
383                streams: vec![s],
384                emitted: false,
385            }
386        }
387    }
388
389    impl PacketSource for MockPacketSource {
390        fn streams(&self) -> &[StreamInfo] {
391            &self.streams
392        }
393        fn next_packet(&mut self) -> Result<Packet> {
394            if self.emitted {
395                return Err(Error::Eof);
396            }
397            self.emitted = true;
398            Ok(Packet::new(0, TimeBase::new(1, 1000), vec![1, 2, 3, 4]))
399        }
400    }
401
402    fn open_packets_mock(_uri: &str) -> Result<Box<dyn PacketSource>> {
403        Ok(Box::new(MockPacketSource::new()))
404    }
405
406    #[test]
407    fn register_packets_and_open_returns_packets_variant() {
408        let mut reg = SourceRegistry::new();
409        reg.register_packets("mockp", open_packets_mock);
410        let out = reg.open("mockp://anything").expect("open");
411        match out {
412            SourceOutput::Packets(mut p) => {
413                assert_eq!(p.streams().len(), 1);
414                let pkt = p.next_packet().expect("first packet");
415                assert_eq!(pkt.data, vec![1, 2, 3, 4]);
416                assert!(matches!(p.next_packet(), Err(Error::Eof)));
417            }
418            _ => panic!("expected SourceOutput::Packets"),
419        }
420    }
421
422    // ---- mock FrameSource ----
423    struct MockFrameSource {
424        params: CodecParameters,
425        emitted: bool,
426    }
427
428    impl MockFrameSource {
429        fn new() -> Self {
430            Self {
431                params: CodecParameters::audio(CodecId::new("pcm_s16le")),
432                emitted: false,
433            }
434        }
435    }
436
437    impl FrameSource for MockFrameSource {
438        fn params(&self) -> &CodecParameters {
439            &self.params
440        }
441        fn next_frame(&mut self) -> Result<Frame> {
442            if self.emitted {
443                return Err(Error::Eof);
444            }
445            self.emitted = true;
446            Ok(Frame::Audio(AudioFrame {
447                samples: 1,
448                pts: Some(0),
449                data: vec![vec![0u8, 0u8]],
450            }))
451        }
452    }
453
454    fn open_frames_mock(_uri: &str) -> Result<Box<dyn FrameSource>> {
455        Ok(Box::new(MockFrameSource::new()))
456    }
457
458    #[test]
459    fn register_frames_and_open_returns_frames_variant() {
460        let mut reg = SourceRegistry::new();
461        reg.register_frames("mockf", open_frames_mock);
462        let out = reg.open("mockf://anything").expect("open");
463        match out {
464            SourceOutput::Frames(mut f) => {
465                assert_eq!(f.params().codec_id.as_str(), "pcm_s16le");
466                let frame = f.next_frame().expect("first frame");
467                match frame {
468                    Frame::Audio(a) => assert_eq!(a.samples, 1),
469                    _ => panic!("expected audio frame"),
470                }
471                assert!(matches!(f.next_frame(), Err(Error::Eof)));
472            }
473            _ => panic!("expected SourceOutput::Frames"),
474        }
475    }
476
477    #[test]
478    fn unknown_scheme_falls_back_to_file_when_registered() {
479        let mut reg = SourceRegistry::new();
480        reg.register_bytes("file", open_bytes_mock);
481        // No `foo` driver — falls through to the `file` driver.
482        let out = reg.open("foo://x").expect("fallback open");
483        assert!(matches!(out, SourceOutput::Bytes(_)));
484    }
485
486    #[test]
487    fn unknown_scheme_with_no_file_driver_errors() {
488        let reg = SourceRegistry::new();
489        let r = reg.open("nope://x");
490        assert!(matches!(r, Err(Error::Unsupported(_))));
491    }
492
493    // ---- mock MultiTitleSource ----
494    struct MockMultiTitleSource {
495        labels: Vec<String>,
496    }
497
498    impl MultiTitleSource for MockMultiTitleSource {
499        fn title_count(&self) -> usize {
500            self.labels.len()
501        }
502        fn open_title(&mut self, index: usize) -> Result<Box<dyn BytesSource>> {
503            if index >= self.labels.len() {
504                return Err(Error::Unsupported(format!(
505                    "no title {index} (have {})",
506                    self.labels.len()
507                )));
508            }
509            // Each title is just its label repeated 4×.
510            let payload = self.labels[index].as_bytes().repeat(4);
511            Ok(Box::new(Cursor::new(payload)))
512        }
513        fn title_label(&self, index: usize) -> String {
514            self.labels[index].clone()
515        }
516        fn title_display_name(&self, index: usize) -> Option<String> {
517            Some(format!("Title {}", self.labels[index]))
518        }
519        fn title_container_hint(&self, _index: usize) -> Option<&'static str> {
520            Some("mpegts")
521        }
522    }
523
524    fn open_multi_title_mock(_uri: &str) -> Result<Box<dyn MultiTitleSource>> {
525        Ok(Box::new(MockMultiTitleSource {
526            labels: vec!["1".to_string(), "2".to_string(), "3".to_string()],
527        }))
528    }
529
530    #[test]
531    fn register_multi_title_and_open_returns_multi_title_variant() {
532        let mut reg = SourceRegistry::new();
533        reg.register_multi_title("mockmt", open_multi_title_mock);
534        let out = reg.open("mockmt://anything").expect("open");
535        match out {
536            SourceOutput::MultiTitle(mut mt) => {
537                assert_eq!(mt.title_count(), 3);
538                assert_eq!(mt.title_label(0), "1");
539                assert_eq!(mt.title_display_name(2).as_deref(), Some("Title 3"));
540                assert_eq!(mt.title_container_hint(0), Some("mpegts"));
541                let mut buf = String::new();
542                mt.open_title(1)
543                    .expect("title 1")
544                    .read_to_string(&mut buf)
545                    .unwrap();
546                assert_eq!(buf, "2222");
547            }
548            _ => panic!("expected SourceOutput::MultiTitle"),
549        }
550    }
551
552    #[test]
553    fn register_overrides_prior_kind() {
554        // Registering `mock` first as bytes then as frames should leave
555        // only the frames opener active (last write wins).
556        let mut reg = SourceRegistry::new();
557        reg.register_bytes("mock", open_bytes_mock);
558        reg.register_frames("mock", open_frames_mock);
559        let out = reg.open("mock://x").expect("open");
560        assert!(matches!(out, SourceOutput::Frames(_)));
561    }
562
563    #[test]
564    fn schemes_iterator_lists_registered() {
565        let mut reg = SourceRegistry::new();
566        reg.register_bytes("mockb", open_bytes_mock);
567        reg.register_packets("mockp", open_packets_mock);
568        reg.register_frames("mockf", open_frames_mock);
569        let mut names: Vec<&str> = reg.schemes().collect();
570        names.sort();
571        assert_eq!(names, vec!["mockb", "mockf", "mockp"]);
572    }
573}