DiscStream::new() replaces open_drive/open_iso/from_reader

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.
This commit is contained in:
Matt Jackson
2026-04-17 15:32:48 +00:00
parent 2674612d87
commit bc62cb35f4
4 changed files with 57 additions and 124 deletions
+7
View File
@@ -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
+1 -1
View File
@@ -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"
+23 -95
View File
@@ -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<u16> = 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<usize>,
opts: &ScanOptions,
) -> io::Result<Self> {
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<u16> = 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<dyn SectorReader>,
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<u16> = 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,
+26 -28
View File
@@ -170,32 +170,13 @@ pub struct InputOptions {
pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
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<Box<dyn crate::pes::S
Some(p) => 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();
}