From bc62cb35f4f33dd14533758bd73e7c6630b60b56 Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:32:31 +0000 Subject: [PATCH] DiscStream::new() replaces open_drive/open_iso/from_reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stream is a stream. DiscStream::new() takes reader + title + keys + batch + format — same pattern as every other stream constructor. Deleted: open_drive(), open_iso(), from_reader() — these were helper functions that chained multiple operations. Library provides primitives, callers decide the sequence. Removed disc:// case from input() — callers use Drive::open() + Disc::scan() + DiscStream::new() directly for disc sources. --- CHANGELOG.md | 7 +++ Cargo.toml | 2 +- src/mux/disc.rs | 118 +++++++++------------------------------------ src/mux/resolve.rs | 54 ++++++++++----------- 4 files changed, 57 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a65e3..269eca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.10.7 (2026-04-17) + +### DiscStream::new() +- Replaced open_drive(), open_iso(), from_reader() with single new() constructor +- Stream accepts ContentFormat and sets up demuxer internally +- Removed disc:// case from input() — callers use primitives directly + ## 0.10.6 (2026-04-16) ### Docker compatibility diff --git a/Cargo.toml b/Cargo.toml index ced9c8f..4d3bfe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.10.6" +version = "0.10.7" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/mux/disc.rs b/src/mux/disc.rs index c5c11b4..c374d88 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -44,101 +44,17 @@ pub struct DiscStream { } impl DiscStream { - /// Open from a physical drive. Caller must have already called - /// drive.wait_ready(), drive.init(), drive.probe_disc(). - /// Drive is moved into the stream — caller manages lock/unlock before/after. - pub fn open_drive( - drive: crate::drive::Drive, - keydb_path: Option<&str>, - title_index: usize, - ) -> crate::error::Result<(Self, Disc)> { - let scan_opts = match keydb_path { - Some(kp) => ScanOptions::with_keydb(kp), - None => ScanOptions::default(), - }; - let mut drive = drive; - let disc = Disc::scan(&mut drive, &scan_opts)?; - - if title_index >= disc.titles.len() { - return Err(crate::error::Error::DiscTitleRange { - index: title_index, - count: disc.titles.len(), - }); - } - - let title = disc.titles[title_index].clone(); - let keys = disc.decrypt_keys(); - let max_batch = detect_max_batch_sectors(drive.device_path()); - let content_format = disc.content_format; - - let mut stream = Self::from_reader(Box::new(drive), title, keys, max_batch); - - // Set demuxer based on content format - match content_format { - crate::disc::ContentFormat::MpegPs => { - stream.ps_demuxer = Some(super::ps::PsDemuxer::new()); - } - crate::disc::ContentFormat::BdTs => { - let pids: Vec = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect(); - if !pids.is_empty() { - stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids)); - } - } - } - Ok((stream, disc)) - } - - /// Open from an ISO file. - pub fn open_iso( - path: &str, - title_index: Option, - opts: &ScanOptions, - ) -> io::Result { - let mut reader = super::iso::IsoSectorReader::open(path)?; - let capacity = reader.capacity(); - - let disc = - Disc::scan_image(&mut reader, capacity, opts).map_err(|e| -> io::Error { e.into() })?; - - if disc.titles.is_empty() { - return Err(crate::error::Error::NoStreams.into()); - } - let idx = title_index.unwrap_or(0); - if idx >= disc.titles.len() { - return Err(crate::error::Error::DiscTitleRange { - index: idx, - count: disc.titles.len(), - } - .into()); - } - - let title = disc.titles[idx].clone(); - let keys = disc.decrypt_keys(); - let batch: u16 = 64; - - let mut stream = Self::from_reader(Box::new(reader), title, keys, batch); - // Set demuxer based on content format - match disc.content_format { - crate::disc::ContentFormat::MpegPs => { - stream.ps_demuxer = Some(super::ps::PsDemuxer::new()); - } - crate::disc::ContentFormat::BdTs => { - let pids: Vec = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect(); - if !pids.is_empty() { - stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids)); - } - } - } - stream.disc = Some(disc); - Ok(stream) - } - - /// Create from any SectorReader + title + keys. - pub fn from_reader( + /// Create a disc stream from any sector reader. + /// + /// Works with physical drives and ISO files — both implement SectorReader. + /// The caller opens the source, scans for titles/keys, and passes them in. + /// The stream handles demuxing, decryption, and codec parsing internally. + pub fn new( reader: Box, title: DiscTitle, decrypt_keys: crate::decrypt::DecryptKeys, batch_sectors: u16, + content_format: crate::disc::ContentFormat, ) -> Self { let extents = title.extents.clone(); @@ -156,6 +72,20 @@ impl DiscStream { parsers.push((pid, super::codec::parser_for_codec(codec))); } + let mut ts_demuxer = None; + let mut ps_demuxer = None; + match content_format { + crate::disc::ContentFormat::MpegPs => { + ps_demuxer = Some(super::ps::PsDemuxer::new()); + } + crate::disc::ContentFormat::BdTs => { + let ts_pids: Vec = pids.clone(); + if !ts_pids.is_empty() { + ts_demuxer = Some(super::ts::TsDemuxer::new(&ts_pids)); + } + } + } + Self { reader, title, @@ -169,10 +99,8 @@ impl DiscStream { batch_sectors, errors: 0, eof: false, - // Demuxer set by caller — open_drive() checks content_format, - // open_iso() always uses TS (Blu-ray ISO). - ts_demuxer: None, - ps_demuxer: None, + ts_demuxer, + ps_demuxer, parsers, pending_frames: std::collections::VecDeque::new(), pid_to_track, diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 82f06a5..6c43732 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -170,32 +170,13 @@ pub struct InputOptions { pub fn input(url: &str, opts: &InputOptions) -> io::Result> { let parsed = parse_url(url); match parsed { - StreamUrl::Disc { device } => { - // Open drive, init, scan — caller manages the drive - let mut drive = match device { - Some(ref d) => { - crate::drive::Drive::open(d).map_err(|e| -> io::Error { e.into() })? - } - None => crate::drive::find_drive().ok_or_else(|| -> io::Error { - crate::error::Error::DeviceNotFound { - path: String::new(), - } - .into() - })?, - }; - let _ = drive.wait_ready(); - let _ = drive.init(); - let _ = drive.probe_disc(); - let (mut stream, _disc) = DiscStream::open_drive( - drive, - opts.keydb_path.as_deref(), - opts.title_index.unwrap_or(0), - ) - .map_err(|e| -> io::Error { e.into() })?; - if opts.raw { - stream.set_raw(); - } - Ok(Box::new(stream)) + StreamUrl::Disc { .. } => { + // Disc sources should use DiscStream::new() directly. + // The caller opens the drive, inits, scans, then creates the stream. + Err(io::Error::new( + io::ErrorKind::Unsupported, + "Use Drive::open() + Disc::scan() + DiscStream::new() for disc sources", + )) } StreamUrl::Iso { ref path } => { validate_file_path(path, "iso")?; @@ -203,8 +184,25 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result crate::disc::ScanOptions::with_keydb(p), None => crate::disc::ScanOptions::default(), }; - let mut stream = - DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?; + let mut reader = super::iso::IsoSectorReader::open(&path.to_string_lossy())?; + let capacity = reader.capacity(); + let disc = crate::disc::Disc::scan_image(&mut reader, capacity, &scan_opts) + .map_err(|e| -> io::Error { e.into() })?; + if disc.titles.is_empty() { + return Err(crate::error::Error::NoStreams.into()); + } + let idx = opts.title_index.unwrap_or(0); + if idx >= disc.titles.len() { + return Err(crate::error::Error::DiscTitleRange { + index: idx, + count: disc.titles.len(), + } + .into()); + } + let title = disc.titles[idx].clone(); + let keys = disc.decrypt_keys(); + let format = disc.content_format; + let mut stream = DiscStream::new(Box::new(reader), title, keys, 64, format); if opts.raw { stream.set_raw(); }