v0.10.1: Streams are PES, Disc::copy() for sector dumps, zero English

Architecture:
- One stream per format, bidirectional PES (read/write on same type)
- IsoStream merged into DiscStream (one type, any SectorReader)
- Disc::copy() for disc→ISO raw sector dump
- IOStream trait deleted, all byte-level Read/Write removed
- ContentReader/OpenDisc/open_title/open_input/open_output deleted
- CountingStream wrapper for progress tracking

Error codes:
- All io::Error English strings replaced with Error enum variants
- From<Error> for io::Error conversion
- Unused variants removed, new stream/mux variants added

Deleted: mkvout.rs, pesout.rs, isowriter.rs, mkv-muxer-plan.md
Updated: all docs, README stream table, CHANGELOG

238 tests, 0 clippy warnings.
This commit is contained in:
MattJackson
2026-04-15 19:46:01 +00:00
parent e6d5fb72a1
commit ed43ced710
31 changed files with 1006 additions and 3689 deletions
+92 -289
View File
@@ -1,42 +1,36 @@
//! DiscStream — read sectors from an optical disc drive.
//! DiscStream — read any disc (physical drive or ISO file) → PES frames.
//!
//! `DiscStream::open()` does the full init sequence:
//! drive open → wait_ready → init → probe_disc → scan
//! One stream type for all disc sources. The source is a SectorReader —
//! Drive (hardware) or IsoSectorReader (file). DiscStream doesn't care.
//!
//! Then reads title extents or full-disc sequentially.
//! No decryption — that's a caller concern.
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
use super::IOStream;
use crate::disc::{
detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions,
};
use crate::drive::Drive;
use crate::error::{Error, Result};
use crate::event::{Event, EventKind};
use std::io::{self, Read, Write};
use std::path::Path;
use crate::sector::SectorReader;
use std::io;
/// Optical disc stream. Read-only — yields raw sector bytes.
/// Disc stream. Reads sectors from any source → PES frames.
///
/// Created from an initialized Drive + title extents or full-disc mode.
/// Error recovery (batch reduction, retry, zero-fill) is handled internally.
/// Sources: physical drive, ISO file, or any SectorReader.
/// Decrypt, demux, and codec parsing happen internally.
pub struct DiscStream {
drive: Drive,
reader: Box<dyn SectorReader>,
title: DiscTitle,
disc: Option<Disc>,
decrypt_keys: crate::decrypt::DecryptKeys,
// What to read
mode: ReadMode,
// Extents to read
extents: Vec<Extent>,
// Position
current_lba: u32,
current_extent: usize,
current_offset: u32,
// Buffer
read_buf: Vec<u8>,
buf_valid: usize,
buf_cursor: usize,
// Batch size for reads
batch_sectors: u16,
@@ -51,77 +45,24 @@ pub struct DiscStream {
pid_to_track: Vec<(u16, usize)>,
}
enum ReadMode {
/// Read title extents (for MKV, M2TS, etc.)
Extents(Vec<Extent>),
/// Read LBA 0 to capacity (for ISO)
Sequential { capacity: u32 },
}
/// Result of opening a DiscStream.
pub struct DiscOpenResult {
pub stream: DiscStream,
pub disc: Disc,
}
impl DiscStream {
/// Open a disc drive, init, scan, and prepare to read a title.
///
/// Steps (each does one thing):
/// 1. Drive::open (or find_drive)
/// 2. wait_ready
/// 3. init (non-fatal)
/// 4. probe_disc (non-fatal)
/// 5. Disc::scan
///
/// Pass an event callback for status reporting, or None.
pub fn open(
device: Option<&Path>,
/// 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,
on_event: Option<&dyn Fn(Event)>,
) -> Result<DiscOpenResult> {
let emit = |kind: EventKind| {
if let Some(cb) = &on_event {
cb(Event { kind });
}
};
// 1. Open
let mut drive = match device {
Some(d) => Drive::open(d)?,
None => crate::drive::find_drive().ok_or_else(|| Error::DeviceNotFound {
path: String::new(),
})?,
};
emit(EventKind::DriveOpened {
device: drive.device_path().to_string(),
});
// 2. Wait
let _ = drive.wait_ready();
emit(EventKind::DriveReady);
// 3. Init
let init_ok = drive.init().is_ok();
emit(EventKind::InitComplete { success: init_ok });
// 4. Probe
let probe_ok = drive.probe_disc().is_ok();
emit(EventKind::ProbeComplete { success: probe_ok });
// 5. Scan
) -> 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)?;
emit(EventKind::ScanComplete {
titles: disc.titles.len(),
});
if title_index >= disc.titles.len() {
return Err(Error::DiscTitleRange {
return Err(crate::error::Error::DiscTitleRange {
index: title_index,
count: disc.titles.len(),
});
@@ -129,45 +70,59 @@ impl DiscStream {
let title = disc.titles[title_index].clone();
let keys = disc.decrypt_keys();
let mut stream = Self::title(drive, title);
stream.decrypt_keys = keys;
let max_batch = detect_max_batch_sectors(drive.device_path());
let content_format = disc.content_format;
// DVD: use program stream demuxer instead of transport stream
if disc.content_format == crate::disc::ContentFormat::MpegPs {
let mut stream = Self::from_reader(Box::new(drive), title, keys, max_batch);
if content_format == crate::disc::ContentFormat::MpegPs {
stream.ts_demuxer = None;
stream.ps_demuxer = Some(super::ps::PsDemuxer::new());
}
Ok(DiscOpenResult { stream, disc })
Ok((stream, disc))
}
/// Create a stream that reads a title's extents.
/// Use this when you already have an initialized Drive.
pub fn title(drive: Drive, title: DiscTitle) -> Self {
let max_batch = detect_max_batch_sectors(drive.device_path());
/// 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);
stream.disc = Some(disc);
Ok(stream)
}
/// Create from any SectorReader + title + keys.
pub fn from_reader(
reader: Box<dyn SectorReader>,
title: DiscTitle,
decrypt_keys: crate::decrypt::DecryptKeys,
batch_sectors: u16,
) -> Self {
let extents = title.extents.clone();
Self::new(drive, title, ReadMode::Extents(extents), max_batch)
}
/// Create a stream that reads the full disc sequentially (for ISO).
pub fn full_disc(drive: Drive, title: DiscTitle, capacity: u32) -> Self {
let max_batch = detect_max_batch_sectors(drive.device_path());
Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch)
}
/// Resume a full disc read from a given LBA (for ISO resume).
/// Use after checking an existing partial file:
/// start_lba = (file_size / 2048) - safety_margin
pub fn full_disc_resume(drive: Drive, title: DiscTitle, capacity: u32, start_lba: u32) -> Self {
let max_batch = detect_max_batch_sectors(drive.device_path());
let mut stream = Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch);
stream.current_lba = start_lba;
stream
}
/// Set SCSI read timeout (default 30s).
fn new(drive: Drive, title: DiscTitle, mode: ReadMode, max_batch: u16) -> Self {
// Set up PES demux from title stream PIDs
let mut pids = Vec::new();
let mut parsers = Vec::new();
let mut pid_to_track = Vec::new();
@@ -183,21 +138,20 @@ impl DiscStream {
}
Self {
drive,
reader,
title,
decrypt_keys: crate::decrypt::DecryptKeys::None,
mode,
current_lba: 0,
disc: None,
decrypt_keys,
extents,
current_extent: 0,
current_offset: 0,
read_buf: Vec::with_capacity(max_batch as usize * 2048),
read_buf: Vec::with_capacity(batch_sectors as usize * 2048),
buf_valid: 0,
buf_cursor: 0,
batch_sectors: max_batch,
batch_sectors,
errors: 0,
eof: false,
ts_demuxer: if pids.is_empty() { None } else { Some(super::ts::TsDemuxer::new(&pids)) },
ps_demuxer: None, // set by caller for DVD content
ps_demuxer: None,
parsers,
pending_frames: std::collections::VecDeque::new(),
pid_to_track,
@@ -209,43 +163,17 @@ impl DiscStream {
self.decrypt_keys = crate::decrypt::DecryptKeys::None;
}
/// Lock the tray.
pub fn lock_tray(&mut self) {
self.drive.lock_tray();
}
/// Unlock the tray.
pub fn unlock_tray(&mut self) {
self.drive.unlock_tray();
}
/// Recover the drive (for batch: switch to another title).
pub fn into_drive(self) -> Drive {
self.drive
}
// ── Fill ─────────────────────────────────────────────────────────────
fn fill(&mut self) -> bool {
match &self.mode {
ReadMode::Extents(_) => self.fill_extents(),
ReadMode::Sequential { .. } => self.fill_sequential(),
}
/// Get the scanned Disc (for listing all titles).
pub fn disc(&self) -> Option<&Disc> {
self.disc.as_ref()
}
fn fill_extents(&mut self) -> bool {
let (ext_start, ext_sectors) = match &self.mode {
ReadMode::Extents(exts) => {
if self.current_extent >= exts.len() {
return false;
}
(
exts[self.current_extent].start_lba,
exts[self.current_extent].sector_count,
)
}
_ => unreachable!(),
};
if self.current_extent >= self.extents.len() {
return false;
}
let ext_start = self.extents[self.current_extent].start_lba;
let ext_sectors = self.extents[self.current_extent].sector_count;
let remaining = ext_sectors.saturating_sub(self.current_offset);
let sectors = remaining.min(self.batch_sectors as u32) as u16;
@@ -253,22 +181,16 @@ impl DiscStream {
if sectors == 0 {
self.current_extent += 1;
self.current_offset = 0;
return self.fill_extents(); // next extent
return self.fill_extents();
}
let lba = ext_start + self.current_offset;
let bytes = sectors as usize * 2048;
self.read_buf.resize(bytes, 0);
// Drive handles all error recovery internally.
match self.drive.read(
lba,
sectors,
&mut self.read_buf[..bytes],
) {
match self.reader.read_sectors(lba, sectors, &mut self.read_buf[..bytes]) {
Ok(_) => {
self.buf_valid = bytes;
self.buf_cursor = 0;
self.current_offset += sectors as u32;
if self.current_offset >= ext_sectors {
self.current_extent += 1;
@@ -276,73 +198,13 @@ impl DiscStream {
}
true
}
Err(_) => false, // drive gone — EOF
Err(_) => false,
}
}
fn fill_sequential(&mut self) -> bool {
let capacity = match &self.mode {
ReadMode::Sequential { capacity } => *capacity,
_ => unreachable!(),
};
if self.current_lba >= capacity {
return false;
}
let remaining = capacity - self.current_lba;
let count = remaining.min(self.batch_sectors as u32) as u16;
let bytes = count as usize * 2048;
self.read_buf.resize(bytes, 0);
// Drive handles all error recovery internally —
// retries, speed changes, zero-fill on unreadable sectors.
match self.drive.read(
self.current_lba,
count,
&mut self.read_buf[..bytes],
) {
Ok(_) => {
self.buf_valid = bytes;
self.buf_cursor = 0;
self.current_lba += count as u32;
true
}
Err(_) => false, // drive gone — EOF
}
}
}
// ── IOStream ─────────────────────────────────────────────────────────────────
impl IOStream for DiscStream {
fn info(&self) -> &DiscTitle {
&self.title
}
fn finish(&mut self) -> io::Result<()> {
self.drive.unlock_tray();
Ok(())
}
fn total_bytes(&self) -> Option<u64> {
match &self.mode {
ReadMode::Extents(extents) => {
Some(extents.iter().map(|e| e.sector_count as u64 * 2048).sum())
}
ReadMode::Sequential { capacity } => Some(*capacity as u64 * 2048),
}
}
fn keys(&self) -> crate::decrypt::DecryptKeys {
self.decrypt_keys.clone()
}
}
impl crate::pes::Stream for DiscStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
// Return buffered frame if available
if let Some(frame) = self.pending_frames.pop_front() {
return Ok(Some(frame));
}
@@ -351,32 +213,22 @@ impl crate::pes::Stream for DiscStream {
return Ok(None);
}
// Read sectors until we produce at least one frame
loop {
// Fill the read buffer with next batch of sectors
let got_data = match &self.mode {
ReadMode::Extents(_) => self.fill_extents(),
ReadMode::Sequential { .. } => self.fill_sequential(),
};
if !got_data {
if !self.fill_extents() {
self.eof = true;
return Ok(None);
}
// Decrypt
let bytes = self.buf_valid;
if let Err(e) = crate::decrypt::decrypt_sectors(
&mut self.read_buf[..bytes],
&self.decrypt_keys,
0,
) {
return Err(io::Error::other(e.to_string()));
return Err(e.into());
}
// Demux into packets, parse into frames
if let Some(ref mut demuxer) = self.ts_demuxer {
// BD: transport stream demux
let packets = demuxer.feed(&self.read_buf[..bytes]);
for pes in &packets {
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
@@ -390,13 +242,11 @@ impl crate::pes::Stream for DiscStream {
}
}
} else if let Some(ref mut demuxer) = self.ps_demuxer {
// DVD: program stream demux
let packets = demuxer.feed(&self.read_buf[..bytes]);
for ps in &packets {
// Map PS stream_id to track index
let track = match ps.stream_id {
0xE0..=0xEF => 0, // video
0xC0..=0xDF => 1, // audio
0xE0..=0xEF => 0,
0xC0..=0xDF => 1,
0xBD => ps.sub_stream_id.map(|s| (s & 0x1F) as usize + 1).unwrap_or(1),
_ => continue,
};
@@ -405,36 +255,28 @@ impl crate::pes::Stream for DiscStream {
self.pending_frames.push_back(crate::pes::PesFrame {
track,
pts: pts_ns,
keyframe: true, // PS doesn't have keyframe flag easily
keyframe: true,
data: ps.data.clone(),
});
}
}
}
// Reset buffer for next read
self.buf_valid = 0;
self.buf_cursor = 0;
if let Some(frame) = self.pending_frames.pop_front() {
return Ok(Some(frame));
}
// No frames produced — read more data
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "disc is read-only"))
Err(crate::error::Error::StreamReadOnly.into())
}
fn finish(&mut self) -> io::Result<()> {
self.drive.unlock_tray();
Ok(())
}
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &DiscTitle {
&self.title
}
fn info(&self) -> &DiscTitle { &self.title }
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
let pid = self.pid_to_track.iter()
@@ -456,42 +298,3 @@ impl crate::pes::Stream for DiscStream {
true
}
}
impl Read for DiscStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// Drain current buffer
if self.buf_cursor < self.buf_valid {
let n = (self.buf_valid - self.buf_cursor).min(buf.len());
buf[..n].copy_from_slice(&self.read_buf[self.buf_cursor..self.buf_cursor + n]);
self.buf_cursor += n;
return Ok(n);
}
if self.eof {
return Ok(0);
}
// Fill next batch
if self.fill() {
let n = self.buf_valid.min(buf.len());
buf[..n].copy_from_slice(&self.read_buf[..n]);
self.buf_cursor = n;
Ok(n)
} else {
self.eof = true;
Ok(0)
}
}
}
impl Write for DiscStream {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"disc is read-only",
))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
+2 -8
View File
@@ -171,10 +171,7 @@ pub fn read_id(r: &mut impl Read) -> io::Result<(u32, usize)> {
4,
))
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid EBML ID",
))
Err(crate::error::Error::MkvInvalid.into())
}
}
@@ -325,10 +322,7 @@ pub fn read_vint(r: &mut impl Read) -> io::Result<(u64, usize)> {
r.read_exact(&mut b)?;
return Ok(((((b0 & 0x3F) as u64) << 8) | b[0] as u64, 2));
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
"unsupported VINT width",
))
Err(crate::error::Error::MkvInvalid.into())
}
// ============================================================
+9 -407
View File
@@ -1,27 +1,16 @@
//! IsoStream — read/write Blu-ray ISO disc images.
//! ISO sector reader — file-backed SectorReader for Blu-ray ISO images.
//!
//! Read: parses UDF filesystem inside the ISO using the same pipeline as
//! DiscStream (titles, streams, labels, AACS). An ISO is a flat image of
//! 2048-byte sectors — sector N starts at byte offset N * 2048.
//!
//! Write: creates a UDF 2.50 filesystem containing the m2ts stream data.
//! The resulting ISO can be mounted or read back via IsoStream.
//! An ISO is a flat image of 2048-byte sectors. Sector N starts at byte offset N * 2048.
//! Used by DiscStream::open_iso() and Disc::scan_image().
use super::isowriter::IsoWriter;
use super::IOStream;
use crate::decrypt::{decrypt_sectors, DecryptKeys};
use crate::disc::{Disc, DiscTitle, ScanOptions};
use crate::error::{Error, Result};
use crate::sector::SectorReader;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
const SECTOR_SIZE: u64 = 2048;
/// Maximum sectors to batch-read at once (64 sectors = 128 KB).
const BATCH_SECTORS: usize = 64;
/// File-backed sector reader for ISO images.
pub struct IsoSectorReader {
file: File,
@@ -29,16 +18,15 @@ pub struct IsoSectorReader {
}
impl IsoSectorReader {
pub fn open(path: &str) -> io::Result<Self> {
pub fn open(path: &str) -> std::io::Result<Self> {
let file = File::open(Path::new(path))
.map_err(|e| io::Error::new(e.kind(), format!("iso://{path}: {e}")))?;
.map_err(|e| std::io::Error::new(e.kind(), format!("iso://{path}: {e}")))?;
let size = file.metadata()?.len();
let sectors = size / SECTOR_SIZE;
if sectors > u32::MAX as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("iso://{path}: image too large ({} TB, max ~8 TB)", size / (1024 * 1024 * 1024 * 1024)),
));
return Err(crate::error::Error::IsoTooLarge {
path: path.to_string(),
}.into());
}
let capacity = sectors as u32;
Ok(Self { file, capacity })
@@ -62,355 +50,6 @@ impl SectorReader for IsoSectorReader {
}
}
/// Blu-ray ISO image stream.
///
/// Read: opens ISO, parses UDF (same as DiscStream), streams BD-TS content.
/// Write: creates UDF 2.50 ISO with BDMV/STREAM/*.m2ts.
pub struct IsoStream {
disc_title: DiscTitle,
disc: Option<Disc>,
// Read side
reader: Option<IsoSectorReader>,
extents: Vec<(u32, u32)>,
extent_idx: usize,
sectors_remaining: u32,
/// Batch buffer: holds up to BATCH_SECTORS sectors (128 KB) at once.
batch_buf: Vec<u8>,
buf_pos: usize,
buf_len: usize,
eof: bool,
/// Decrypt on read — auto-detected from disc scan.
decrypt_keys: DecryptKeys,
// Write side
iso_writer: Option<IsoWriter<io::BufWriter<File>>>,
write_started: bool,
// PES output (for InputStream impl)
demuxer: Option<super::ts::TsDemuxer>,
parsers: Vec<(u16, Box<dyn super::codec::CodecParser>)>,
pending_frames: std::collections::VecDeque<crate::pes::PesFrame>,
pid_to_track: Vec<(u16, usize)>,
}
impl IsoStream {
/// Open an ISO file for reading. Parses UDF, scans titles, streams, labels.
pub fn open(path: &str, title_index: Option<usize>, opts: &ScanOptions) -> io::Result<Self> {
let mut reader = IsoSectorReader::open(path)?;
let capacity = reader.capacity();
let disc = Disc::scan_image(&mut reader, capacity, opts)
.map_err(|e| io::Error::other(e.to_string()))?;
if disc.titles.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no titles found in ISO image",
));
}
let idx = title_index.unwrap_or(0);
if idx >= disc.titles.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"title {} out of range (disc has {})",
idx + 1,
disc.titles.len()
),
));
}
let disc_title = disc.titles[idx].clone();
let decrypt_keys = disc.decrypt_keys();
let extents: Vec<(u32, u32)> = disc_title
.extents
.iter()
.map(|e| (e.start_lba, e.sector_count))
.collect();
let sectors_remaining = extents.first().map(|e| e.1).unwrap_or(0);
// Set up PES demux from title stream PIDs
let mut pids = Vec::new();
let mut parsers: Vec<(u16, Box<dyn super::codec::CodecParser>)> = Vec::new();
let mut pid_to_track = Vec::new();
for (i, s) in disc_title.streams.iter().enumerate() {
let (pid, codec) = match s {
crate::disc::Stream::Video(v) => (v.pid, v.codec),
crate::disc::Stream::Audio(a) => (a.pid, a.codec),
crate::disc::Stream::Subtitle(s) => (s.pid, s.codec),
};
pids.push(pid);
pid_to_track.push((pid, i));
parsers.push((pid, super::codec::parser_for_codec(codec)));
}
Ok(IsoStream {
disc_title,
disc: Some(disc),
reader: Some(reader),
extents,
extent_idx: 0,
sectors_remaining,
batch_buf: vec![0u8; BATCH_SECTORS * SECTOR_SIZE as usize],
buf_pos: 0,
buf_len: 0,
eof: false,
decrypt_keys,
iso_writer: None,
write_started: false,
demuxer: if pids.is_empty() { None } else { Some(super::ts::TsDemuxer::new(&pids)) },
parsers,
pending_frames: std::collections::VecDeque::new(),
pid_to_track,
})
}
/// Create an ISO file for writing.
pub fn create(path: &str) -> io::Result<Self> {
let file = File::create(Path::new(path))
.map_err(|e| io::Error::new(e.kind(), format!("iso://{path}: {e}")))?;
let buf_writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
let iso_writer = IsoWriter::new(buf_writer, "FREEMKV", "00001.m2ts");
Ok(IsoStream {
disc_title: DiscTitle::empty(),
disc: None,
decrypt_keys: DecryptKeys::None,
reader: None,
extents: Vec::new(),
extent_idx: 0,
sectors_remaining: 0,
batch_buf: Vec::new(),
buf_pos: 0,
buf_len: 0,
eof: false,
iso_writer: Some(iso_writer),
write_started: false,
demuxer: None,
parsers: Vec::new(),
pending_frames: std::collections::VecDeque::new(),
pid_to_track: Vec::new(),
})
}
/// Set metadata (for write mode). Must be called before writing data.
pub fn meta(mut self, dt: &DiscTitle) -> Self {
self.disc_title = dt.clone();
// Update the ISO writer's volume ID and m2ts filename from title metadata
if let Some(writer) = self.iso_writer.take() {
let vol_id = if dt.playlist.is_empty() {
"FREEMKV".to_string()
} else {
dt.playlist
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == ' ')
.collect::<String>()
};
let m2ts_name = format!("{:05}.m2ts", dt.playlist_id.max(1));
self.iso_writer = Some(writer.with_names(&vol_id, &m2ts_name));
}
self
}
/// Get the full Disc (for listing all titles).
pub fn disc(&self) -> Option<&Disc> {
self.disc.as_ref()
}
/// Read up to BATCH_SECTORS sectors at once into the batch buffer.
fn read_next_batch(&mut self) -> io::Result<bool> {
let reader = match self.reader.as_mut() {
Some(r) => r,
None => return Ok(false),
};
if self.extent_idx >= self.extents.len() {
return Ok(false);
}
let (start_lba, total) = self.extents[self.extent_idx];
let offset = total - self.sectors_remaining;
let lba = start_lba + offset;
// Read up to BATCH_SECTORS, but no more than remaining in this extent
let count = (self.sectors_remaining as usize).min(BATCH_SECTORS) as u16;
reader
.read_sectors(lba, count, &mut self.batch_buf)
.map_err(|e| io::Error::other(e.to_string()))?;
// Decrypt after read — stream handles its own decryption
let bytes = count as usize * SECTOR_SIZE as usize;
decrypt_sectors(&mut self.batch_buf[..bytes], &self.decrypt_keys, 0)
.map_err(|e| io::Error::other(e.to_string()))?;
self.buf_pos = 0;
self.buf_len = bytes;
self.sectors_remaining -= count as u32;
if self.sectors_remaining == 0 {
self.extent_idx += 1;
if self.extent_idx < self.extents.len() {
self.sectors_remaining = self.extents[self.extent_idx].1;
}
}
Ok(true)
}
/// Skip decryption — return raw encrypted bytes.
pub fn set_raw(&mut self) {
self.decrypt_keys = DecryptKeys::None;
}
}
impl crate::pes::Stream for IsoStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
// Return buffered frame
if let Some(frame) = self.pending_frames.pop_front() {
return Ok(Some(frame));
}
if self.eof {
return Ok(None);
}
// Read until we produce at least one frame
loop {
if !self.read_next_batch()? {
self.eof = true;
return Ok(None);
}
// Data in batch_buf is already decrypted by fill_next_batch
if let Some(ref mut demuxer) = self.demuxer {
let packets = demuxer.feed(&self.batch_buf[..self.buf_len]);
for pes in &packets {
if let Some((pid_idx, _)) = self.pid_to_track.iter().enumerate()
.find(|(_, (pid, _))| *pid == pes.pid)
{
let track_idx = self.pid_to_track[pid_idx].1;
if let Some((_, parser)) = self.parsers.iter_mut()
.find(|(pid, _)| *pid == pes.pid)
{
for frame in parser.parse(pes) {
self.pending_frames.push_back(
crate::pes::PesFrame::from_codec_frame(track_idx, frame)
);
}
}
}
}
}
if let Some(frame) = self.pending_frames.pop_front() {
return Ok(Some(frame));
}
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "ISO is read-only for PES"))
}
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &crate::disc::DiscTitle {
&self.disc_title
}
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
let pid = self.pid_to_track.iter()
.find(|(_, idx)| *idx == track)
.map(|(pid, _)| *pid)?;
self.parsers.iter()
.find(|(p, _)| *p == pid)
.and_then(|(_, parser)| parser.codec_private())
}
fn headers_ready(&self) -> bool {
for (idx, s) in self.disc_title.streams.iter().enumerate() {
if let crate::disc::Stream::Video(v) = s {
if !v.secondary && self.codec_private(idx).is_none() {
return false;
}
}
}
true
}
}
impl IOStream for IsoStream {
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if let Some(ref mut w) = self.iso_writer {
w.finish()?;
}
Ok(())
}
fn total_bytes(&self) -> Option<u64> {
if self.reader.is_some() {
Some(self.disc_title.size_bytes)
} else {
None
}
}
fn keys(&self) -> DecryptKeys {
self.decrypt_keys.clone()
}
}
impl Read for IsoStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.eof {
return Ok(0);
}
if self.buf_pos < self.buf_len {
let n = (self.buf_len - self.buf_pos).min(buf.len());
buf[..n].copy_from_slice(&self.batch_buf[self.buf_pos..self.buf_pos + n]);
self.buf_pos += n;
return Ok(n);
}
if self.read_next_batch()? {
let n = self.buf_len.min(buf.len());
buf[..n].copy_from_slice(&self.batch_buf[..n]);
self.buf_pos = n;
Ok(n)
} else {
self.eof = true;
Ok(0)
}
}
}
impl Write for IsoStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let w = match self.iso_writer.as_mut() {
Some(w) => w,
None => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"iso:// opened for reading — cannot write",
))
}
};
if !self.write_started {
w.start()?;
self.write_started = true;
}
w.write_data(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -453,41 +92,4 @@ mod tests {
std::fs::remove_file(&dir).ok();
}
#[test]
fn iso_write_creates_valid_udf() {
let path = std::env::temp_dir().join("freemkv_test_iso_write.iso");
let mut stream = IsoStream::create(path.to_str().unwrap()).unwrap();
// Write some fake BD-TS content
let mut content = Vec::new();
for i in 0..100u8 {
let mut pkt = [0u8; 192];
pkt[4] = 0x47;
pkt[5] = i;
content.extend_from_slice(&pkt);
}
stream.write_all(&content).unwrap();
stream.finish().unwrap();
// Verify the ISO has valid UDF structure
let file = File::open(&path).unwrap();
let size = file.metadata().unwrap().len();
assert!(size > 288 * SECTOR_SIZE); // at least header + some data
// Read back and verify AVDP at sector 256
let mut reader = IsoSectorReader::open(path.to_str().unwrap()).unwrap();
let mut avdp = [0u8; 2048];
reader.read_sectors(256, 1, &mut avdp).unwrap();
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
assert_eq!(tag_id, 2, "AVDP tag should be 2");
// Verify VRS at sector 16
let mut vrs = [0u8; 2048];
reader.read_sectors(16, 1, &mut vrs).unwrap();
assert_eq!(&vrs[1..6], b"BEA01");
std::fs::remove_file(&path).ok();
}
}
-678
View File
@@ -1,678 +0,0 @@
//! UDF ISO writer — creates Blu-ray disc images.
//!
//! Writes a minimal UDF 2.50 filesystem containing BDMV/STREAM/*.m2ts.
//! The ISO can be mounted or read back via IsoStream.
//!
//! Layout:
//! Sector 0-15: System area (zeros)
//! Sector 16-18: Volume Recognition Sequence (BEA01, NSR03, TEA01)
//! Sector 32-37: Volume Descriptor Sequence
//! Sector 256: Anchor Volume Descriptor Pointer
//! Sector 260-271: Metadata partition (FSD, ICBs, directories)
//! Sector 288+: File data (m2ts content)
//! Last-256: Reserve AVDP
use std::io::{self, Seek, SeekFrom, Write};
const SECTOR_SIZE: u64 = 2048;
// Layout constants
const VRS_START: u32 = 16; // Volume Recognition Sequence
const VDS_START: u32 = 32; // Volume Descriptor Sequence
const AVDP_SECTOR: u32 = 256; // Anchor Volume Descriptor Pointer
const PARTITION_START: u32 = 257; // Physical partition start
const METADATA_START: u32 = 260; // Metadata partition content
const FSD_SECTOR: u32 = 260; // File Set Descriptor
const ROOT_ICB_SECTOR: u32 = 261; // Root directory ICB
const ROOT_DIR_SECTOR: u32 = 262; // Root directory data
const BDMV_ICB_SECTOR: u32 = 263; // BDMV/ ICB
const BDMV_DIR_SECTOR: u32 = 264; // BDMV/ directory data
const STREAM_ICB_SECTOR: u32 = 265; // BDMV/STREAM/ ICB
const STREAM_DIR_SECTOR: u32 = 266; // BDMV/STREAM/ directory data
const M2TS_ICB_SECTOR: u32 = 267; // m2ts file ICB
const DATA_START: u32 = 288; // Start of file data (aligned)
/// Write a complete BD ISO image.
///
/// Writes UDF structure, then streams m2ts content from the writer.
/// Call `start()` first, then write BD-TS bytes, then call `finish()`.
pub struct IsoWriter<W: Write + Seek> {
writer: W,
volume_id: String,
m2ts_name: String,
data_start_sector: u32,
bytes_written: u64,
}
impl<W: Write + Seek> IsoWriter<W> {
/// Create a new ISO writer. Call `start()` to write the UDF header.
pub fn new(writer: W, volume_id: &str, m2ts_name: &str) -> Self {
Self {
writer,
volume_id: volume_id.to_string(),
m2ts_name: m2ts_name.to_string(),
data_start_sector: DATA_START,
bytes_written: 0,
}
}
/// Update volume ID and m2ts filename. Must be called before `start()`.
pub fn with_names(mut self, volume_id: &str, m2ts_name: &str) -> Self {
self.volume_id = volume_id.to_string();
self.m2ts_name = m2ts_name.to_string();
self
}
/// Write UDF filesystem header. After this, write m2ts content bytes.
pub fn start(&mut self) -> io::Result<()> {
// System area: sectors 0-15 (zeros)
let zero_sector = [0u8; SECTOR_SIZE as usize];
for _ in 0..VRS_START {
self.writer.write_all(&zero_sector)?;
}
// Volume Recognition Sequence
self.write_vrs()?;
// Pad sectors 19-31
for _ in 19..VDS_START {
self.writer.write_all(&zero_sector)?;
}
// Volume Descriptor Sequence (sectors 32-37)
self.write_vds()?;
// Pad sectors 38-255
for _ in 38..AVDP_SECTOR {
self.writer.write_all(&zero_sector)?;
}
// AVDP at sector 256
self.write_avdp()?;
// Partition area: metadata file ICB at partition_start
self.write_metadata_file_icb()?;
// Pad to metadata start
for _ in (PARTITION_START + 1)..METADATA_START {
self.writer.write_all(&zero_sector)?;
}
// Metadata partition
self.write_fsd()?;
self.write_root_icb()?;
self.write_root_dir()?;
self.write_bdmv_icb()?;
self.write_bdmv_dir()?;
self.write_stream_icb()?;
self.write_stream_dir()?;
self.write_m2ts_icb(0)?; // placeholder size, updated in finish()
// Pad to data start
for _ in (M2TS_ICB_SECTOR + 1)..self.data_start_sector {
self.writer.write_all(&zero_sector)?;
}
Ok(())
}
/// Write m2ts content bytes. Call after `start()`.
pub fn write_data(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.writer.write(buf)?;
self.bytes_written += n as u64;
Ok(n)
}
/// Finalize the ISO: pad to sector boundary, update file sizes, write reserve AVDP.
pub fn finish(&mut self) -> io::Result<()> {
// Pad to sector boundary
let remainder = (self.bytes_written % SECTOR_SIZE) as usize;
if remainder > 0 {
let pad = SECTOR_SIZE as usize - remainder;
let zeros = vec![0u8; pad];
self.writer.write_all(&zeros)?;
self.bytes_written += pad as u64;
}
let total_data_sectors = (self.bytes_written / SECTOR_SIZE) as u32;
let total_sectors = self.data_start_sector + total_data_sectors;
// Seek back and update m2ts file ICB with actual size
self.writer
.seek(SeekFrom::Start(M2TS_ICB_SECTOR as u64 * SECTOR_SIZE))?;
self.write_m2ts_icb(self.bytes_written)?;
// Seek to end and write reserve AVDP
let reserve_sector = if total_sectors > 512 {
total_sectors - 256
} else {
total_sectors.saturating_sub(1).max(AVDP_SECTOR + 1)
};
self.writer
.seek(SeekFrom::Start(reserve_sector as u64 * SECTOR_SIZE))?;
self.write_avdp()?;
self.writer.flush()?;
Ok(())
}
// ── UDF structure writers ──────────────────────────────────────────────
fn write_vrs(&mut self) -> io::Result<()> {
// BEA01 at sector 16
let mut bea = [0u8; SECTOR_SIZE as usize];
bea[0] = 0; // structure type
bea[1..6].copy_from_slice(b"BEA01");
bea[6] = 1; // structure version
self.writer.write_all(&bea)?;
// NSR03 at sector 17 (UDF 2.50)
let mut nsr = [0u8; SECTOR_SIZE as usize];
nsr[0] = 0;
nsr[1..6].copy_from_slice(b"NSR03");
nsr[6] = 1;
self.writer.write_all(&nsr)?;
// TEA01 at sector 18
let mut tea = [0u8; SECTOR_SIZE as usize];
tea[0] = 0;
tea[1..6].copy_from_slice(b"TEA01");
tea[6] = 1;
self.writer.write_all(&tea)?;
Ok(())
}
fn write_vds(&mut self) -> io::Result<()> {
// Primary Volume Descriptor (tag 1) at sector 32
let mut pvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut pvd, 1, VDS_START);
// Volume Identifier at offset 24 (32-byte d-string)
write_dstring(&mut pvd[24..56], &self.volume_id);
self.writer.write_all(&pvd)?;
// Partition Descriptor (tag 5) at sector 33
let mut pd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut pd, 5, VDS_START + 1);
// Partition starting location at offset 188
pd[188..192].copy_from_slice(&PARTITION_START.to_le_bytes());
// Partition length (large enough for everything)
let part_len: u32 = 0xFFFF_FFFF;
pd[192..196].copy_from_slice(&part_len.to_le_bytes());
self.writer.write_all(&pd)?;
// Logical Volume Descriptor (tag 6) at sector 34
let mut lvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut lvd, 6, VDS_START + 2);
// Logical block size at offset 212
lvd[212..216].copy_from_slice(&2048u32.to_le_bytes());
// Number of partition maps at offset 268
lvd[268..272].copy_from_slice(&2u32.to_le_bytes());
// Partition map 1: Type 1 (physical), 6 bytes
lvd[440] = 1; // type
lvd[441] = 6; // length
// Partition map 2: Type 2 (metadata), 64 bytes
lvd[446] = 2; // type
lvd[447] = 64; // length
// Entity ID for metadata partition
lvd[450..473].copy_from_slice(b"*UDF Metadata Partition");
self.writer.write_all(&lvd)?;
// Unallocated Space Descriptor (tag 7) at sector 35
let mut usd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut usd, 7, VDS_START + 3);
self.writer.write_all(&usd)?;
// Implementation Use Volume Descriptor (tag 4) at sector 36
let mut iuvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut iuvd, 4, VDS_START + 4);
self.writer.write_all(&iuvd)?;
// Terminating Descriptor (tag 8) at sector 37
let mut td = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut td, 8, VDS_START + 5);
self.writer.write_all(&td)?;
Ok(())
}
fn write_avdp(&mut self) -> io::Result<()> {
let mut avdp = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut avdp, 2, AVDP_SECTOR);
// Main VDS extent_ad: {length, location} per UDF spec
avdp[16..20].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
avdp[20..24].copy_from_slice(&VDS_START.to_le_bytes()); // location
// Reserve VDS extent_ad (same as main for simplicity)
avdp[24..28].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
avdp[28..32].copy_from_slice(&VDS_START.to_le_bytes()); // location
self.writer.write_all(&avdp)?;
Ok(())
}
fn write_metadata_file_icb(&mut self) -> io::Result<()> {
// Extended File Entry (tag 266) at partition_start
// Points to metadata content at METADATA_START
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, PARTITION_START);
// ICB tag at offset 16
icb[16..20].copy_from_slice(&0u32.to_le_bytes()); // prior recorded
icb[20..22].copy_from_slice(&0u16.to_le_bytes()); // strategy type
icb[22..24].copy_from_slice(&0u16.to_le_bytes()); // strategy parameter
// File type at offset 27: 250 = metadata file
icb[27] = 250;
// Information length at offset 56
let meta_len: u64 = 12 * SECTOR_SIZE; // 12 sectors of metadata
icb[56..64].copy_from_slice(&meta_len.to_le_bytes());
// Extended attribute length at offset 208
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
// Allocation descriptor at offset 216: short_ad (length + position)
let ad_len = meta_len as u32;
let ad_pos = METADATA_START - PARTITION_START; // relative to partition
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_fsd(&mut self) -> io::Result<()> {
let mut fsd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut fsd, 256, FSD_SECTOR);
// Root Directory ICB (long_ad at offset 400)
let root_lba = ROOT_ICB_SECTOR - METADATA_START; // metadata-relative
fsd[400..404].copy_from_slice(&SECTOR_SIZE.to_le_bytes()[..4]); // extent length
fsd[404..408].copy_from_slice(&root_lba.to_le_bytes());
self.writer.write_all(&fsd)?;
Ok(())
}
fn write_root_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, ROOT_ICB_SECTOR);
icb[27] = 4; // file type: directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = ROOT_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_root_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
// Parent entry (.. points to self)
offset += write_fid(
&mut dir[offset..],
ROOT_ICB_SECTOR - METADATA_START,
"",
true,
);
// BDMV directory entry
offset += write_fid(
&mut dir[offset..],
BDMV_ICB_SECTOR - METADATA_START,
"BDMV",
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_bdmv_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, BDMV_ICB_SECTOR);
icb[27] = 4; // directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = BDMV_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_bdmv_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
offset += write_fid(
&mut dir[offset..],
ROOT_ICB_SECTOR - METADATA_START,
"",
true,
);
offset += write_fid(
&mut dir[offset..],
STREAM_ICB_SECTOR - METADATA_START,
"STREAM",
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_stream_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, STREAM_ICB_SECTOR);
icb[27] = 4; // directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = STREAM_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_stream_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
offset += write_fid(
&mut dir[offset..],
BDMV_ICB_SECTOR - METADATA_START,
"",
true,
);
offset += write_fid(
&mut dir[offset..],
M2TS_ICB_SECTOR - METADATA_START,
&self.m2ts_name,
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_m2ts_icb(&mut self, file_size: u64) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, M2TS_ICB_SECTOR);
icb[27] = 5; // file type: regular file
icb[56..64].copy_from_slice(&file_size.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
// Allocation: data starts at DATA_START in the physical partition.
// UDF short_ad is 30 bits for extent length (max 1 GB = 0x3FFFFFFF).
// For files > 1 GB, write multiple short_ad entries of 1 GB each plus remainder.
let data_offset = self.data_start_sector - PARTITION_START;
const MAX_EXTENT: u64 = 0x3FFF_FFFF; // 1 GB - 1 (30-bit max)
let mut remaining = file_size;
let mut ad_offset: usize = 216;
let mut sector_pos = data_offset;
while remaining > 0 && ad_offset + 8 <= SECTOR_SIZE as usize {
let extent_len = if remaining > MAX_EXTENT {
MAX_EXTENT
} else {
remaining
};
icb[ad_offset..ad_offset + 4].copy_from_slice(&(extent_len as u32).to_le_bytes());
icb[ad_offset + 4..ad_offset + 8].copy_from_slice(&sector_pos.to_le_bytes());
ad_offset += 8; // each short_ad is 8 bytes
let extent_sectors = extent_len.div_ceil(SECTOR_SIZE) as u32;
sector_pos += extent_sectors;
remaining -= extent_len;
}
self.writer.write_all(&icb)?;
Ok(())
}
}
// ── UDF primitives ─────────────────────────────────────────────────────────
/// Write a UDF Descriptor Tag at the start of a sector.
fn write_descriptor_tag(buf: &mut [u8], tag_id: u16, sector: u32) {
buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
// Descriptor version: 3 (UDF 2.50)
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
// Tag location
buf[12..16].copy_from_slice(&sector.to_le_bytes());
// Compute CRC-CCITT over descriptor body (bytes 16+)
let body = &buf[16..];
let body_len = body.len();
let crc = udf_crc(body);
buf[8..10].copy_from_slice(&crc.to_le_bytes());
// Descriptor CRC length
buf[10..12].copy_from_slice(&(body_len as u16).to_le_bytes());
// Compute tag checksum: sum of bytes 0-3, 5-15 mod 256
buf[4] = 0; // clear before computing
let checksum: u8 = buf[0..4]
.iter()
.chain(buf[5..16].iter())
.fold(0u8, |acc, &b| acc.wrapping_add(b));
buf[4] = checksum;
}
/// UDF CRC-CCITT (CRC-16/ECMA-182 polynomial 0x11021).
fn udf_crc(data: &[u8]) -> u16 {
// CRC lookup table for polynomial 0x11021
static CRC_TABLE: [u16; 256] = {
let mut table = [0u16; 256];
let mut i = 0;
while i < 256 {
let mut crc = (i as u16) << 8;
let mut j = 0;
while j < 8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
};
let mut crc: u16 = 0;
for &byte in data {
crc = (crc << 8) ^ CRC_TABLE[((crc >> 8) as u8 ^ byte) as usize];
}
crc
}
/// Write a UDF d-string (compressed unicode string with length prefix).
fn write_dstring(buf: &mut [u8], s: &str) {
let max = buf.len() - 1; // last byte is length
let bytes = s.as_bytes();
let len = bytes.len().min(max);
if len > 0 {
buf[0] = 8; // compression ID: 8 = Latin-1
buf[1..1 + len].copy_from_slice(&bytes[..len]);
buf[buf.len() - 1] = (len + 1) as u8; // d-string length including comp ID
}
}
/// Write a File Identifier Descriptor. Returns bytes written (4-byte aligned).
fn write_fid(buf: &mut [u8], icb_lba: u32, name: &str, is_parent: bool) -> usize {
// Tag 257 = File Identifier Descriptor
let name_bytes = name.as_bytes();
let name_len = if is_parent { 0 } else { name_bytes.len() + 1 }; // +1 for comp ID
let fid_len = 38 + name_len; // fixed header + identifier
let padded = (fid_len + 3) & !3; // 4-byte align
if padded > buf.len() {
return 0;
}
// Tag
buf[0..2].copy_from_slice(&257u16.to_le_bytes());
// File version number at offset 16
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
// File characteristics at offset 18
buf[18] = if is_parent { 0x0A } else { 0x02 }; // parent | directory
if !is_parent && !name.contains('.') {
buf[18] = 0x02; // directory
} else if !is_parent {
buf[18] = 0x00; // file
}
// ICB (long_ad at offset 20): extent length + location
buf[20..24].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
buf[24..28].copy_from_slice(&icb_lba.to_le_bytes());
// Identifier length at offset 36
buf[36] = name_len as u8;
// Implementation use length at offset 37
buf[37] = 0;
// File identifier at offset 38
if !is_parent && !name_bytes.is_empty() {
buf[38] = 8; // compression ID: Latin-1
buf[39..39 + name_bytes.len()].copy_from_slice(name_bytes);
}
padded
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
/// Read a little-endian u16 from a byte slice at the given offset.
fn le_u16(data: &[u8], off: usize) -> u16 {
u16::from_le_bytes([data[off], data[off + 1]])
}
/// Read a little-endian u32 from a byte slice at the given offset.
#[allow(dead_code)]
fn le_u32(data: &[u8], off: usize) -> u32 {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
/// Read a little-endian u64 from a byte slice at the given offset.
fn le_u64(data: &[u8], off: usize) -> u64 {
u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
])
}
/// Get the sector at a given sector number from the output data.
fn sector(data: &[u8], num: u32) -> &[u8] {
let start = num as usize * SECTOR_SIZE as usize;
&data[start..start + SECTOR_SIZE as usize]
}
#[test]
fn isowriter_creates_valid_udf() {
let buf = Cursor::new(Vec::new());
let mut w = IsoWriter::new(buf, "TEST_VOL", "00001.m2ts");
w.start().unwrap();
w.write_data(&[0xAA; 4096]).unwrap();
w.finish().unwrap();
let data = w.writer.into_inner();
// AVDP at sector 256 should have tag ID = 2
let avdp = sector(&data, AVDP_SECTOR);
assert_eq!(le_u16(avdp, 0), 2, "AVDP tag ID should be 2");
// VRS at sector 16 should contain "BEA01"
let vrs = sector(&data, VRS_START);
assert_eq!(&vrs[1..6], b"BEA01", "VRS sector 16 should contain BEA01");
// FSD at metadata sector should have tag ID = 256
let fsd = sector(&data, FSD_SECTOR);
assert_eq!(le_u16(fsd, 0), 256, "FSD tag ID should be 256");
}
#[test]
fn isowriter_updates_file_size() {
let buf = Cursor::new(Vec::new());
let mut w = IsoWriter::new(buf, "SIZE_TEST", "00001.m2ts");
w.start().unwrap();
let test_data = vec![0x42u8; 8192]; // exactly 4 sectors
let written = w.write_data(&test_data).unwrap();
assert_eq!(written, 8192);
w.finish().unwrap();
let data = w.writer.into_inner();
// Read m2ts ICB at M2TS_ICB_SECTOR and check information length at offset 56
let icb = sector(&data, M2TS_ICB_SECTOR);
let file_size = le_u64(icb, 56);
assert_eq!(
file_size, 8192,
"m2ts ICB file size should match bytes written (8192), got {}",
file_size
);
}
#[test]
fn isowriter_with_names() {
let buf = Cursor::new(Vec::new());
let mut w = IsoWriter::new(buf, "MY_DISC", "00042.m2ts");
w.start().unwrap();
w.write_data(&[0x00; 2048]).unwrap();
w.finish().unwrap();
let data = w.writer.into_inner();
// Check PVD (sector 32) volume_id at offset 24 as d-string
let pvd = sector(&data, VDS_START);
// d-string: byte 0 = compression ID (8), then ASCII chars
assert_eq!(pvd[24], 8, "PVD volume_id compression ID should be 8");
assert_eq!(
&pvd[25..32],
b"MY_DISC",
"PVD should contain volume_id 'MY_DISC'"
);
// Check STREAM directory (sector 266) for m2ts filename in FID
let stream_dir = sector(&data, STREAM_DIR_SECTOR);
// The FID for the m2ts file should contain the filename after the parent entry.
// Search for "00042.m2ts" in the sector data
let name = b"00042.m2ts";
let found = stream_dir.windows(name.len()).any(|w| w == name);
assert!(
found,
"STREAM directory should contain m2ts filename '00042.m2ts'"
);
}
#[test]
fn isowriter_empty_content() {
let buf = Cursor::new(Vec::new());
let mut w = IsoWriter::new(buf, "EMPTY", "00001.m2ts");
w.start().unwrap();
// No data written
w.finish().unwrap();
let data = w.writer.into_inner();
// Should still have valid UDF structure
// AVDP at sector 256
let avdp = sector(&data, AVDP_SECTOR);
assert_eq!(
le_u16(avdp, 0),
2,
"AVDP tag should be present even with no data"
);
// VRS
let vrs = sector(&data, VRS_START);
assert_eq!(&vrs[1..6], b"BEA01");
// FSD
let fsd = sector(&data, FSD_SECTOR);
assert_eq!(le_u16(fsd, 0), 256);
// m2ts ICB should show 0 file size
let icb = sector(&data, M2TS_ICB_SECTOR);
let file_size = le_u64(icb, 56);
assert_eq!(file_size, 0, "empty content should have 0 file size");
// Output should be at least DATA_START sectors (the header structure)
assert!(
data.len() >= DATA_START as usize * SECTOR_SIZE as usize,
"output too small for valid UDF structure"
);
}
}
+41 -99
View File
@@ -1,9 +1,9 @@
//! M2tsStream — BD transport stream with embedded metadata header.
//!
//! Write: prepends FMKV metadata header, then passes through BD-TS bytes.
//! Read: extracts metadata header (or scans PMT), then yields BD-TS bytes.
//! Write: prepends FMKV metadata header, then muxes PES frames into BD-TS.
//! Read: extracts metadata header (or scans PMT), then demuxes BD-TS into PES frames.
use super::{meta, ts, IOStream};
use super::{meta, ts};
use crate::disc::{DiscTitle, Stream as DiscStream};
use std::io::{self, Read, Write};
@@ -14,8 +14,7 @@ const SCAN_SIZE: usize = 1024 * 1024;
enum Mode {
Write {
writer: Box<dyn Write>,
header_written: bool,
muxer: super::tsmux::TsMuxer<Box<dyn Write>>,
},
Read {
reader: Box<dyn Read>,
@@ -41,9 +40,6 @@ fn read_fill(r: &mut impl Read, buf: &mut [u8]) -> io::Result<usize> {
pub struct M2tsStream {
disc_title: DiscTitle,
mode: Mode,
finished: bool,
/// Content size in bytes (file size minus header), set for read mode.
content_size: Option<u64>,
// PES support
demuxer: Option<ts::TsDemuxer>,
parsers: Vec<(u16, Box<dyn super::codec::CodecParser>)>,
@@ -55,23 +51,36 @@ pub struct M2tsStream {
}
impl M2tsStream {
/// Create for writing. Metadata header is written on first write().
pub fn new(writer: impl Write + 'static) -> Self {
Self {
disc_title: DiscTitle::empty(),
mode: Mode::Write {
writer: Box::new(writer),
header_written: false,
},
finished: false,
content_size: None,
/// Create for writing PES frames → BD-TS output.
/// Writes FMKV metadata header, then muxes PES frames into BD transport stream.
pub fn create(mut writer: impl Write + 'static, title: &DiscTitle) -> io::Result<Self> {
// Write FMKV metadata header
if !title.streams.is_empty() {
let m = meta::M2tsMeta::from_title(title);
meta::write_header(&mut writer, &m)?;
}
let pids: Vec<u16> = title.streams.iter().map(|s| match s {
DiscStream::Video(v) => v.pid,
DiscStream::Audio(a) => a.pid,
DiscStream::Subtitle(s) => s.pid,
}).collect();
let boxed: Box<dyn Write> = Box::new(writer);
let mut muxer = super::tsmux::TsMuxer::new(boxed, &pids);
for (i, cp) in title.codec_privates.iter().enumerate() {
if let Some(data) = cp {
muxer.set_codec_private(i, data.clone());
}
}
Ok(Self {
disc_title: title.clone(),
mode: Mode::Write { muxer },
demuxer: None,
parsers: Vec::new(),
pending_frames: std::collections::VecDeque::new(),
pid_to_track: Vec::new(),
pes_eof: false,
stored_codec_privates: Vec::new(),
}
})
}
fn setup_pes(streams: &[DiscStream]) -> PesSetup {
@@ -91,12 +100,6 @@ impl M2tsStream {
(pids, parsers, pid_to_track)
}
/// Set stream metadata. Returns self for chaining.
pub fn meta(mut self, dt: &DiscTitle) -> Self {
self.disc_title = dt.clone();
self
}
/// Open an M2TS stream for reading. Takes any Read source — file, pipe, socket.
///
/// Tries FMKV metadata header first. Falls back to PMT scan of first 1 MB.
@@ -118,8 +121,6 @@ impl M2tsStream {
return Ok(Self {
disc_title: title.clone(),
mode: Mode::Read { reader: chain },
finished: false,
content_size: None,
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
parsers,
pending_frames: std::collections::VecDeque::new(),
@@ -131,7 +132,7 @@ impl M2tsStream {
// No FMKV header — scan head for PMT
let streams = ts::scan_streams(&head)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no streams found"))?;
.ok_or_else(|| -> io::Error { crate::error::Error::NoStreams.into() })?;
let (pids, parsers, pid_to_track) = Self::setup_pes(&streams);
@@ -145,8 +146,6 @@ impl M2tsStream {
..DiscTitle::empty()
},
mode: Mode::Read { reader: chain },
finished: false,
content_size: None,
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
parsers,
pending_frames: std::collections::VecDeque::new(),
@@ -167,7 +166,7 @@ impl crate::pes::Stream for M2tsStream {
loop {
let reader = match &mut self.mode {
Mode::Read { reader } => reader,
_ => return Err(io::Error::new(io::ErrorKind::Unsupported, "not in read mode")),
_ => return Err(crate::error::Error::StreamWriteOnly.into()),
};
let mut buf = vec![0u8; 192 * 1024];
let n = reader.read(&mut buf)?;
@@ -197,11 +196,19 @@ impl crate::pes::Stream for M2tsStream {
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "use M2tsOutputStream for writing"))
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
match &mut self.mode {
Mode::Write { muxer } => muxer.write_frame(frame.track, frame.pts, &frame.data),
Mode::Read { .. } => Err(crate::error::Error::StreamReadOnly.into()),
}
}
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn finish(&mut self) -> io::Result<()> {
match &mut self.mode {
Mode::Write { muxer } => muxer.finish(),
Mode::Read { .. } => Ok(()),
}
}
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
@@ -231,68 +238,3 @@ impl crate::pes::Stream for M2tsStream {
}
}
impl IOStream for M2tsStream {
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if self.finished {
return Ok(());
}
self.finished = true;
if let Mode::Write { ref mut writer, .. } = self.mode {
writer.flush()
} else {
Ok(())
}
}
fn total_bytes(&self) -> Option<u64> {
self.content_size
}
}
impl Write for M2tsStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.mode {
Mode::Write {
ref mut writer,
ref mut header_written,
} => {
if !*header_written {
if !self.disc_title.streams.is_empty() {
let m = meta::M2tsMeta::from_title(&self.disc_title);
meta::write_header(&mut *writer, &m)?;
}
*header_written = true;
}
writer.write(buf)
}
Mode::Read { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for reading",
)),
}
}
fn flush(&mut self) -> io::Result<()> {
if let Mode::Write { ref mut writer, .. } = self.mode {
writer.flush()
} else {
Ok(())
}
}
}
impl Read for M2tsStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.mode {
Mode::Read { ref mut reader } => reader.read(buf),
Mode::Write { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for writing",
)),
}
}
}
+1 -1
View File
@@ -247,7 +247,7 @@ pub fn read_header(r: &mut impl Read) -> io::Result<Option<M2tsMeta>> {
r.read_exact(&mut len_buf)?;
let json_len = u32::from_be_bytes(len_buf) as usize;
if json_len > MAX_JSON_SIZE {
return Err(io::Error::new(io::ErrorKind::InvalidData, "FMKV JSON too large"));
return Err(crate::error::Error::NoMetadata.into());
}
let mut json_buf = vec![0u8; json_len];
-70
View File
@@ -1,70 +0,0 @@
//! MKV output stream — accepts PES frames, writes Matroska container.
//!
//! Implements OutputStream. Takes PES frames directly — no TS demuxing needed.
//! Creates the MKV muxer once codec_private is provided for all tracks.
use super::mkv::{MkvMuxer, MkvTrack};
use super::WriteSeek;
use crate::disc::DiscTitle;
use crate::pes::PesFrame;
use std::io;
pub struct MkvOutputStream {
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
title: DiscTitle,
}
impl MkvOutputStream {
/// Create an MKV output stream. Codec privates come from title.codec_privates.
pub fn create(
writer: Box<dyn WriteSeek>,
title: &DiscTitle,
) -> io::Result<Self> {
let mut tracks = Vec::new();
for (idx, s) in title.streams.iter().enumerate() {
let mut track = match s {
crate::disc::Stream::Video(v) => MkvTrack::video(v),
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
crate::disc::Stream::Subtitle(s) => MkvTrack::subtitle(s),
};
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
track.codec_private = Some(cp.clone());
}
tracks.push(track);
}
let muxer = MkvMuxer::new_with_chapters(
writer,
&tracks,
Some(&title.playlist),
title.duration_secs,
&title.chapters,
)?;
Ok(Self { muxer: Some(muxer), title: title.clone() })
}
}
impl crate::pes::Stream for MkvOutputStream {
fn read(&mut self) -> io::Result<Option<PesFrame>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "MKV output is write-only"))
}
fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
if let Some(ref mut muxer) = self.muxer {
muxer.write_frame(frame.track, frame.pts, frame.keyframe, &frame.data)
} else {
Ok(())
}
}
fn finish(&mut self) -> io::Result<()> {
if let Some(muxer) = self.muxer.take() {
muxer.finish()
} else {
Ok(())
}
}
fn info(&self) -> &DiscTitle { &self.title }
}
+56 -496
View File
@@ -1,14 +1,10 @@
//! MkvStream — Matroska container stream.
//!
//! Write: BD-TS bytes in → demux → codec parse → MKV container out.
//! Read: MKV container in → extract frames → wrap as BD-TS → bytes out.
//! Read: MKV container → demux EBML → PES frames out.
//! Write: PES frames in → MKV mux → Matroska container.
use super::codec::{self, CodecParser};
use super::lookahead::{LookaheadBuffer, LookaheadState, DEFAULT_LOOKAHEAD_SIZE};
use super::mkv::{MkvMuxer, MkvTrack};
use super::ts::TsDemuxer;
use super::{ebml, IOStream, WriteSeek};
use std::io::Seek;
use super::{ebml, WriteSeek};
type MkvHeaderResult = io::Result<(crate::disc::DiscTitle, Vec<(u16, Vec<u8>)>)>;
@@ -19,44 +15,19 @@ fn skip_bytes(r: &mut impl Read, n: u64) -> io::Result<()> {
}
use crate::disc::*;
use std::io::{self, Read, Write};
/// Lookahead buffer for codec header detection (5 MB default).
const DEFAULT_MAX_BUFFER: usize = DEFAULT_LOOKAHEAD_SIZE;
#[derive(Debug, Clone, Copy, PartialEq)]
enum WritePhase {
Scanning,
Streaming,
}
struct WriteState {
demuxer: TsDemuxer,
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
writer: Option<Box<dyn WriteSeek>>,
parsers: Vec<(u16, Box<dyn CodecParser>)>,
pid_to_track: Vec<(u16, usize)>,
tracks: Vec<MkvTrack>,
lookahead: LookaheadBuffer,
phase: WritePhase,
video_pending: usize,
}
use std::io::{self, Read};
struct ReadState {
reader: Box<dyn Read>,
buf: Vec<u8>,
pos: usize,
len: usize,
cluster_ts_ms: i64,
/// Codec private data per track (track_number, hvcC/avcC bytes).
/// Emitted as Annex B NALs before first frame of each video track.
codec_privates: Vec<(u16, Vec<u8>)>,
/// Tracks that have already had their codec_private emitted.
initialized_tracks: Vec<u16>,
}
enum Mode {
Write(Box<WriteState>),
Write {
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
},
Read(ReadState),
}
@@ -64,95 +35,52 @@ enum Mode {
pub struct MkvStream {
disc_title: DiscTitle,
mode: Mode,
max_buffer: usize,
finished: bool,
/// File size in bytes, set for read mode.
file_size: Option<u64>,
}
impl MkvStream {
/// Create for writing. BD-TS bytes written to this stream produce MKV output.
pub fn new(writer: impl Write + Seek + 'static) -> Self {
Self {
disc_title: DiscTitle::empty(),
mode: Mode::Write(Box::new(WriteState {
demuxer: TsDemuxer::new(&[]),
muxer: None,
writer: Some(Box::new(writer)),
parsers: Vec::new(),
pid_to_track: Vec::new(),
tracks: Vec::new(),
lookahead: LookaheadBuffer::new(DEFAULT_MAX_BUFFER),
phase: WritePhase::Scanning,
video_pending: 0,
})),
max_buffer: DEFAULT_MAX_BUFFER,
finished: false,
file_size: None,
}
}
/// Set stream metadata. Returns self for chaining.
pub fn meta(mut self, dt: &DiscTitle) -> Self {
if let Mode::Write(ref mut ws) = self.mode {
let mut pids = Vec::new();
for s in &dt.streams {
let (pid, track, parser) = match s {
crate::disc::Stream::Video(v) => {
// Only count primary video as pending — secondary streams
// (Dolby Vision EL, PiP) may never produce codec headers
if !v.secondary {
ws.video_pending += 1;
}
(v.pid, MkvTrack::video(v), codec::parser_for_codec(v.codec))
}
crate::disc::Stream::Audio(a) => {
(a.pid, MkvTrack::audio(a), codec::parser_for_codec(a.codec))
}
crate::disc::Stream::Subtitle(s) => (
s.pid,
MkvTrack::subtitle(s),
codec::parser_for_codec_with_data(s.codec, s.codec_data.clone()),
),
};
let idx = ws.tracks.len();
pids.push(pid);
ws.pid_to_track.push((pid, idx));
ws.parsers.push((pid, parser));
ws.tracks.push(track);
/// Create for writing PES frames → MKV container.
/// Codec privates come from title.codec_privates (populated by input stream).
pub fn create(
writer: Box<dyn WriteSeek>,
title: &DiscTitle,
) -> io::Result<Self> {
let mut tracks = Vec::new();
for (idx, s) in title.streams.iter().enumerate() {
let mut track = match s {
crate::disc::Stream::Video(v) => MkvTrack::video(v),
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
crate::disc::Stream::Subtitle(s) => MkvTrack::subtitle(s),
};
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
track.codec_private = Some(cp.clone());
}
ws.demuxer = TsDemuxer::new(&pids);
tracks.push(track);
}
self.disc_title = dt.clone();
self
let muxer = MkvMuxer::new_with_chapters(
writer,
&tracks,
Some(&title.playlist),
title.duration_secs,
&title.chapters,
)?;
Ok(Self {
disc_title: title.clone(),
mode: Mode::Write { muxer: Some(muxer) },
})
}
/// Set lookahead buffer size. Returns self.
pub fn max_buffer(mut self, size: usize) -> Self {
self.max_buffer = size;
if let Mode::Write(ref mut ws) = self.mode {
ws.lookahead = LookaheadBuffer::new(size);
}
self
}
/// Open an MKV file for reading.
/// Open an MKV file for reading → PES frames.
pub fn open(mut reader: impl Read + 'static) -> io::Result<Self> {
let (disc_title, codec_privates) = parse_mkv_header(&mut reader)?;
Ok(Self {
disc_title,
mode: Mode::Read(ReadState {
reader: Box::new(reader),
buf: Vec::new(),
pos: 0,
len: 0,
cluster_ts_ms: 0,
codec_privates,
initialized_tracks: Vec::new(),
}),
max_buffer: 0,
finished: false,
file_size: None,
})
}
}
@@ -161,7 +89,7 @@ impl crate::pes::Stream for MkvStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
let rs = match self.mode {
Mode::Read(ref mut rs) => rs,
Mode::Write(_) => return Err(io::Error::new(io::ErrorKind::Unsupported, "write-only")),
Mode::Write { .. } => return Err(crate::error::Error::StreamWriteOnly.into()),
};
loop {
@@ -209,11 +137,22 @@ impl crate::pes::Stream for MkvStream {
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "use MkvOutputStream for writing"))
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
match &mut self.mode {
Mode::Write { muxer: Some(ref mut m) } => m.write_frame(frame.track, frame.pts, frame.keyframe, &frame.data),
Mode::Write { muxer: None } => Ok(()),
Mode::Read(_) => Err(crate::error::Error::StreamReadOnly.into()),
}
}
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn finish(&mut self) -> io::Result<()> {
if let Mode::Write { ref mut muxer } = self.mode {
if let Some(m) = muxer.take() {
m.finish()?;
}
}
Ok(())
}
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
@@ -234,256 +173,6 @@ impl crate::pes::Stream for MkvStream {
}
}
impl IOStream for MkvStream {
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if self.finished {
return Ok(());
}
self.finished = true;
if let Mode::Write(ref mut ws) = self.mode {
// Flush remaining PES packets
if let Some(ref mut muxer) = ws.muxer {
for pes in &ws.demuxer.flush() {
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
}
}
// Write cues and finalize
if let Some(muxer) = ws.muxer.take() {
muxer.finish()?;
}
}
Ok(())
}
fn total_bytes(&self) -> Option<u64> {
self.file_size
}
}
// ── Write ──────────────────────────────────────────────────────
impl Write for MkvStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let dt = &self.disc_title;
let ws = match self.mode {
Mode::Write(ref mut ws) => ws,
Mode::Read(_) => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for reading",
))
}
};
match ws.phase {
WritePhase::Scanning => {
// Feed demuxer for codec detection
let packets = ws.demuxer.feed(buf);
for pes in &packets {
if let Some((_, p)) = ws.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
let _ = p.parse(pes);
}
}
let state = ws.lookahead.push(buf);
// Check if all video codec headers found
if check_codec_private(ws) {
ws.lookahead.mark_ready();
begin_streaming(ws, dt)?;
return Ok(buf.len());
}
match state {
LookaheadState::Collecting | LookaheadState::Ready => Ok(buf.len()),
LookaheadState::Overflow => Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"no codec headers found within lookahead buffer",
)),
}
}
WritePhase::Streaming => {
let packets = ws.demuxer.feed(buf);
if let Some(ref mut muxer) = ws.muxer {
for pes in &packets {
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
}
}
Ok(buf.len())
}
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
// ── Read ───────────────────────────────────────────────────────
impl Read for MkvStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let rs = match self.mode {
Mode::Read(ref mut rs) => rs,
Mode::Write(_) => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for writing",
))
}
};
// Drain internal buffer first
if rs.pos < rs.len {
let n = (rs.len - rs.pos).min(buf.len());
buf[..n].copy_from_slice(&rs.buf[rs.pos..rs.pos + n]);
rs.pos += n;
return Ok(n);
}
// Read next element from MKV
loop {
let (id, size, _) = match ebml::read_element_header(&mut rs.reader) {
Ok(h) => h,
Err(_) => return Ok(0),
};
match id {
ebml::CLUSTER => continue,
ebml::CLUSTER_TIMESTAMP => {
rs.cluster_ts_ms = ebml::read_uint_val(&mut rs.reader, size as usize)? as i64;
continue;
}
ebml::SIMPLE_BLOCK => {
let block = ebml::read_binary_val(&mut rs.reader, size as usize)?;
if block.len() < 4 {
continue;
}
let (track, vl) = block_vint(&block);
if vl + 3 > block.len() {
continue;
}
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
let frame = &block[vl + 3..];
let pts_ms = rs.cluster_ts_ms + rel_ts as i64;
let tnum = track as u16;
rs.buf.clear();
// First frame of a track: emit codec_private as Annex B NALs
if !rs.initialized_tracks.contains(&tnum) {
rs.initialized_tracks.push(tnum);
if let Some((_, cp)) = rs.codec_privates.iter().find(|(t, _)| *t == tnum) {
let annex_b = hvcc_to_annex_b(cp);
if !annex_b.is_empty() {
frame_to_ts(&mut rs.buf, tnum, pts_ms, &annex_b);
}
}
}
frame_to_ts(&mut rs.buf, tnum, pts_ms, frame);
rs.pos = 0;
rs.len = rs.buf.len();
if rs.len > 0 {
let n = rs.len.min(buf.len());
buf[..n].copy_from_slice(&rs.buf[..n]);
rs.pos = n;
return Ok(n);
}
}
_ => {
if size != u64::MAX && size > 0 {
skip_bytes(&mut rs.reader, size)?;
}
}
}
}
}
}
// ── Write internals ────────────────────────────────────────────
fn check_codec_private(ws: &mut WriteState) -> bool {
if ws.video_pending == 0 {
return true;
}
for (pid, parser) in &ws.parsers {
if let Some(cp) = parser.codec_private() {
if let Some((_, idx)) = ws.pid_to_track.iter().find(|(p, _)| p == pid) {
if ws.tracks[*idx].codec_private.is_none() {
ws.tracks[*idx].codec_private = Some(cp);
ws.video_pending -= 1;
}
}
}
}
ws.video_pending == 0
}
fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
let writer = ws
.writer
.take()
.ok_or_else(|| io::Error::other("writer already consumed"))?;
ws.muxer = Some(MkvMuxer::new_with_chapters(
writer,
&ws.tracks,
Some(&dt.playlist),
dt.duration_secs,
&dt.chapters,
)?);
ws.phase = WritePhase::Streaming;
// Re-parse buffered data through a fresh demuxer, then reset the main
// demuxer so stale PES assembler state from scanning doesn't cause
// duplicate or incomplete packets during streaming.
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
let buffered = ws.lookahead.drain();
if !buffered.is_empty() {
let mut temp = TsDemuxer::new(&pids);
let packets = temp.feed(&buffered);
if let Some(ref mut muxer) = ws.muxer {
for pes in &packets {
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
}
}
}
// Transfer remainder bytes from old demuxer to new one.
// Preserves 192-byte packet alignment across the reset.
let remainder = ws.demuxer.take_remainder();
ws.demuxer = TsDemuxer::new(&pids);
ws.demuxer.set_remainder(remainder);
Ok(())
}
fn write_pes(
pid_to_track: &[(u16, usize)],
parsers: &mut [(u16, Box<dyn CodecParser>)],
muxer: &mut MkvMuxer<Box<dyn WriteSeek>>,
pes: &super::ts::PesPacket,
) -> io::Result<()> {
let idx = match pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
Some((_, idx)) => *idx,
None => return Ok(()),
};
let parser = match parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
Some((_, p)) => p,
None => return Ok(()),
};
for frame in parser.parse(pes) {
muxer.write_frame(idx, frame.pts_ns, frame.keyframe, &frame.data)?;
}
Ok(())
}
// ── MKV header parsing (read side) ────────────────────────────
/// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
@@ -498,19 +187,16 @@ fn parse_mkv_header(
let (id, size, _) = ebml::read_element_header(r)?;
if id != ebml::EBML {
return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML"));
return Err(crate::error::Error::MkvInvalid.into());
}
if size > i64::MAX as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"EBML header too large",
));
return Err(crate::error::Error::MkvInvalid.into());
}
skip_bytes(r, size)?;
let (id, _, _) = ebml::read_element_header(r)?;
if id != ebml::SEGMENT {
return Err(io::Error::new(io::ErrorKind::InvalidData, "no Segment"));
return Err(crate::error::Error::MkvInvalid.into());
}
let (mut got_info, mut got_tracks) = (false, false);
@@ -647,7 +333,7 @@ fn parse_track(
SampleRate::S48
};
// Map MKV track numbers to BD-TS PIDs (same mapping as frame_to_ts)
// Map MKV track numbers to BD-TS PIDs
let ts_pid = if tnum == 1 { 0x1011 } else { 0x1100 + (tnum - 2) };
let stream = match ttype {
@@ -685,8 +371,6 @@ fn parse_track(
Ok((stream, tnum, codec_priv))
}
// ── BD-TS frame wrapping (read side) ──────────────────────────
fn block_vint(d: &[u8]) -> (u64, usize) {
if d.is_empty() {
return (0, 0);
@@ -705,127 +389,3 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
}
(0, 1) // Unsupported 5+ byte VINT — treat as track 0
}
/// Convert HEVCDecoderConfigurationRecord (hvcC) to Annex B NAL units.
/// Extracts VPS, SPS, PPS arrays and prefixes each with 0x00000001.
fn hvcc_to_annex_b(hvcc: &[u8]) -> Vec<u8> {
// hvcC format (ISO 14496-15):
// byte 0: configurationVersion (1)
// bytes 1-21: profile/level info
// byte 22: numOfArrays
// For each array:
// byte 0: array_completeness(1) + reserved(1) + NAL_unit_type(6)
// bytes 1-2: numNalus (big-endian u16)
// For each NAL:
// bytes 0-1: nalUnitLength (big-endian u16)
// bytes 2..: NAL data
if hvcc.len() < 23 {
return Vec::new();
}
let num_arrays = hvcc[22] as usize;
let mut pos = 23;
let mut out = Vec::new();
for _ in 0..num_arrays {
if pos + 3 > hvcc.len() {
break;
}
pos += 1; // skip array_completeness + NAL type byte
let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
pos += 2;
for _ in 0..num_nalus {
if pos + 2 > hvcc.len() {
break;
}
let nal_len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
pos += 2;
if pos + nal_len > hvcc.len() {
break;
}
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
out.extend_from_slice(&hvcc[pos..pos + nal_len]);
pos += nal_len;
}
}
out
}
fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
let pid = if track == 1 {
0x1011
} else {
0x1100 + (track - 2)
};
let is_video = track <= 1 || pid == 0x1011;
let stream_id: u8 = if is_video { 0xE0 } else { 0xBD };
let pts = encode_pts(pts_ms * 90);
let hdr = [0x00, 0x00, 0x01, stream_id, 0x00, 0x00, 0x80, 0x80, 0x05];
let mut pes = Vec::with_capacity(hdr.len() + pts.len() + data.len());
pes.extend_from_slice(&hdr);
pes.extend_from_slice(&pts);
// Video: convert MKV length-prefixed NALs to Annex B start codes
if is_video && data.len() > 4 {
let mut pos = 0;
while pos + 4 <= data.len() {
let nal_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if nal_len == 0 || pos + nal_len > data.len() {
break;
}
pes.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
pes.extend_from_slice(&data[pos..pos + nal_len]);
pos += nal_len;
}
} else {
pes.extend_from_slice(data);
}
let mut off = 0;
let mut pusi = true;
while off < pes.len() {
let mut pkt = [0u8; 192];
pkt[4] = 0x47;
pkt[5] = (pid >> 8) as u8 & 0x1F;
if pusi {
pkt[5] |= 0x40;
pusi = false;
}
pkt[6] = pid as u8;
let space = 184;
let rem = pes.len() - off;
let n = rem.min(space);
if n < space {
let pad = space - n;
pkt[7] = 0x30; // AF + payload
pkt[8] = pad as u8;
if pad > 1 {
pkt[9] = 0x00;
}
for byte in pkt.iter_mut().take((8 + pad).min(192)).skip(10) {
*byte = 0xFF;
}
pkt[8 + pad..8 + pad + n].copy_from_slice(&pes[off..off + n]);
} else {
pkt[7] = 0x10; // payload only
pkt[8..8 + n].copy_from_slice(&pes[off..off + n]);
}
out.extend_from_slice(&pkt);
off += n;
}
}
fn encode_pts(pts: i64) -> [u8; 5] {
let p = pts as u64;
[
0x21 | ((p >> 29) & 0x0E) as u8,
((p >> 22) & 0xFF) as u8,
0x01 | ((p >> 14) & 0xFE) as u8,
((p >> 7) & 0xFF) as u8,
0x01 | ((p << 1) & 0xFE) as u8,
]
}
+16 -48
View File
@@ -1,37 +1,29 @@
//! Stream-based I/O pipeline.
//!
//! All formats are streams. Two URLs, left reads, right writes:
//! All formats are PES streams. Read from a format → PES frames.
//! Write PES frames → a format.
//!
//! ```text
//! freemkv disc:// mkv://Dune.mkv
//! freemkv m2ts://Dune.m2ts mkv://Dune.mkv
//! freemkv disc:// network://10.1.7.11:9000
//! freemkv disc:// stdio://
//! freemkv stdio:// mkv://Dune.mkv
//! ```
//!
//! Streams implement `IOStream` for uniform handling:
//!
//! ```text
//! let mut input = open_input("disc://", &opts)?;
//! let mut output = open_output("mkv://Dune.mkv", input.info())?;
//! io::copy(&mut *input, &mut *output)?;
//! let mut input = input("iso://Disc.iso", &opts)?;
//! let title = input.info().clone();
//! let mut output = output("mkv://Dune.mkv", &title)?;
//! while let Ok(Some(frame)) = input.read() {
//! output.write(&frame)?;
//! }
//! output.finish()?;
//! ```
//!
//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
pub mod codec;
pub mod disc;
pub mod ebml;
pub mod iso;
mod isowriter;
pub mod lookahead;
mod m2ts;
pub mod meta;
pub mod mkv;
pub mod mkvout;
pub mod pesout;
pub mod tsmux;
pub mod tsreader;
mod m2ts;
pub mod meta;
mod mkvstream;
pub mod network;
pub mod null;
@@ -40,40 +32,16 @@ pub mod resolve;
pub mod stdio;
pub mod ts;
pub use disc::{DiscOpenResult, DiscStream};
pub use iso::{IsoSectorReader, IsoStream};
pub use disc::DiscStream;
pub use iso::IsoSectorReader;
pub use m2ts::M2tsStream;
pub use mkvstream::MkvStream;
pub use network::NetworkStream;
pub use null::NullStream;
pub use resolve::{input, output, open_input, open_output, parse_url, InputOptions, StreamUrl};
pub use resolve::{input, output, parse_url, InputOptions, StreamUrl};
pub use stdio::StdioStream;
use crate::disc::DiscTitle;
use std::io::{self, Read, Seek, Write};
/// Common interface for all stream types.
///
/// A stream can be opened for reading or created for writing.
/// Calling the unsupported direction returns an error.
pub trait IOStream: Read + Write {
/// Get stream metadata.
fn info(&self) -> &DiscTitle;
/// Finalize the stream (flush, write index/cues, close).
fn finish(&mut self) -> io::Result<()>;
/// Total content size in bytes, if known. Used for progress display.
fn total_bytes(&self) -> Option<u64> {
None
}
/// Decryption keys for this stream. Default: no encryption.
/// Overridden by DiscStream and IsoStream for AACS/CSS.
fn keys(&self) -> crate::decrypt::DecryptKeys {
crate::decrypt::DecryptKeys::None
}
}
use std::io::{Seek, Write};
// WriteSeek — used internally by MKV muxer (container format requires seeking).
pub trait WriteSeek: Write + Seek {}
+3 -8
View File
@@ -60,12 +60,7 @@ impl NetworkStream {
// Read FMKV metadata header
let disc_title = meta::read_header(&mut reader)?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"no FMKV metadata header from sender",
)
})?
.ok_or_else(|| -> io::Error { crate::error::Error::NoMetadata.into() })?
.to_title();
Ok(Self {
@@ -79,7 +74,7 @@ impl crate::pes::Stream for NetworkStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
match &mut self.mode {
Mode::Read { reader } => crate::pes::PesFrame::deserialize(reader),
_ => Err(io::Error::new(io::ErrorKind::Unsupported, "network opened for writing")),
_ => Err(crate::error::Error::StreamWriteOnly.into()),
}
}
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
@@ -94,7 +89,7 @@ impl crate::pes::Stream for NetworkStream {
}
frame.serialize(writer)
}
_ => Err(io::Error::new(io::ErrorKind::Unsupported, "network opened for reading")),
_ => Err(crate::error::Error::StreamReadOnly.into()),
}
}
fn finish(&mut self) -> io::Result<()> {
+10 -100
View File
@@ -1,114 +1,24 @@
//! NullStream — discards all data. Write-only. For benchmarking.
//! NullStream — discards all data. Write-only PES sink. For benchmarking.
use super::IOStream;
use crate::disc::DiscTitle;
use std::io::{self, Read, Write};
use std::io;
/// Null stream — accepts writes, discards data. For benchmarking rip speed.
/// Null stream — accepts PES writes, discards data. For benchmarking rip speed.
pub struct NullStream {
disc_title: DiscTitle,
bytes_written: u64,
}
impl Default for NullStream {
fn default() -> Self {
Self::new()
}
}
impl NullStream {
pub fn new() -> Self {
pub fn new(title: &DiscTitle) -> Self {
Self {
disc_title: DiscTitle::empty(),
bytes_written: 0,
disc_title: title.clone(),
}
}
pub fn meta(mut self, dt: &DiscTitle) -> Self {
self.disc_title = dt.clone();
self
}
pub fn bytes_written(&self) -> u64 {
self.bytes_written
}
}
impl IOStream for NullStream {
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Write for NullStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.bytes_written += buf.len() as u64;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Read for NullStream {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"null stream is write-only",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn null_counts_bytes() {
let mut ns = NullStream::new();
assert_eq!(ns.bytes_written(), 0);
ns.write_all(&[0u8; 100]).unwrap();
assert_eq!(ns.bytes_written(), 100);
ns.write_all(&[1u8; 50]).unwrap();
assert_eq!(ns.bytes_written(), 150);
// Single write returns correct count
let n = ns.write(&[0u8; 200]).unwrap();
assert_eq!(n, 200);
assert_eq!(ns.bytes_written(), 350);
}
#[test]
fn null_read_errors() {
let mut ns = NullStream::new();
let mut buf = [0u8; 10];
let err = ns.read(&mut buf).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
}
#[test]
fn null_finish_ok() {
let mut ns = NullStream::new();
ns.write_all(&[0u8; 1000]).unwrap();
ns.finish().unwrap();
}
#[test]
fn null_implements_iostream() {
let ns = NullStream::new();
let mut boxed: Box<dyn IOStream> = Box::new(ns);
boxed.write_all(&[0u8; 50]).unwrap();
let info = boxed.info();
assert_eq!(info.streams.len(), 0);
boxed.finish().unwrap();
}
#[test]
fn null_total_bytes_returns_none() {
let ns = NullStream::new();
assert_eq!(ns.total_bytes(), None);
}
impl crate::pes::Stream for NullStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { Ok(None) }
fn write(&mut self, _: &crate::pes::PesFrame) -> io::Result<()> { Ok(()) }
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &DiscTitle { &self.disc_title }
}
-124
View File
@@ -1,124 +0,0 @@
//! PES output streams — each writes its own format from PES frames.
use super::tsmux::TsMuxer;
use crate::disc::DiscTitle;
use crate::pes::PesFrame;
use std::io::{self, Write};
// ── M2TS ────────────────────────────────────────────────────────────────────
pub struct M2tsOutputStream {
muxer: TsMuxer<io::BufWriter<std::fs::File>>,
title: DiscTitle,
}
impl M2tsOutputStream {
pub fn create(path: &str, title: &DiscTitle) -> io::Result<Self> {
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(), format!("m2ts://{}: {}", path, e)))?;
let mut writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
if !title.streams.is_empty() {
let m = super::meta::M2tsMeta::from_title(title);
super::meta::write_header(&mut writer, &m)?;
}
let pids = extract_pids(title);
let mut muxer = TsMuxer::new(writer, &pids);
for (i, cp) in title.codec_privates.iter().enumerate() {
if let Some(data) = cp {
muxer.set_codec_private(i, data.clone());
}
}
Ok(Self { muxer, title: title.clone() })
}
}
impl crate::pes::Stream for M2tsOutputStream {
fn read(&mut self) -> io::Result<Option<PesFrame>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "M2TS output is write-only"))
}
fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
self.muxer.write_frame(frame.track, frame.pts, &frame.data)
}
fn finish(&mut self) -> io::Result<()> { self.muxer.finish() }
fn info(&self) -> &DiscTitle { &self.title }
}
// ── Null ────────────────────────────────────────────────────────────────────
pub struct NullOutputStream { title: DiscTitle }
impl NullOutputStream {
pub fn new(title: &DiscTitle) -> Self { Self { title: title.clone() } }
}
impl crate::pes::Stream for NullOutputStream {
fn read(&mut self) -> io::Result<Option<PesFrame>> { Ok(None) }
fn write(&mut self, _: &PesFrame) -> io::Result<()> { Ok(()) }
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &DiscTitle { &self.title }
}
// ── Stdio — serializes PES frames directly ──────────────────────────────────
pub struct StdioOutputStream {
writer: io::BufWriter<io::Stdout>,
title: DiscTitle,
}
impl StdioOutputStream {
pub fn new(title: &DiscTitle) -> Self {
Self { writer: io::BufWriter::new(io::stdout()), title: title.clone() }
}
}
impl crate::pes::Stream for StdioOutputStream {
fn read(&mut self) -> io::Result<Option<PesFrame>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "stdio output is write-only"))
}
fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
frame.serialize(&mut self.writer)
}
fn finish(&mut self) -> io::Result<()> { self.writer.flush() }
fn info(&self) -> &DiscTitle { &self.title }
}
// ── Network — serializes PES frames over TCP ────────────────────────────────
pub struct NetworkOutputStream {
writer: io::BufWriter<std::net::TcpStream>,
title: DiscTitle,
}
impl NetworkOutputStream {
pub fn connect(addr: &str, title: &DiscTitle) -> io::Result<Self> {
let stream = std::net::TcpStream::connect(addr)?;
let mut writer = io::BufWriter::with_capacity(256 * 1024, stream);
if !title.streams.is_empty() {
let m = super::meta::M2tsMeta::from_title(title);
super::meta::write_header(&mut writer, &m)?;
writer.flush()?;
}
Ok(Self { writer, title: title.clone() })
}
}
impl crate::pes::Stream for NetworkOutputStream {
fn read(&mut self) -> io::Result<Option<PesFrame>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "network output is write-only"))
}
fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
frame.serialize(&mut self.writer)
}
fn finish(&mut self) -> io::Result<()> { self.writer.flush() }
fn info(&self) -> &DiscTitle { &self.title }
}
// ── Helpers ─────────────────────────────────────────────────────────────────
fn extract_pids(title: &DiscTitle) -> Vec<u16> {
title.streams.iter().map(|s| match s {
crate::disc::Stream::Video(v) => v.pid,
crate::disc::Stream::Audio(a) => a.pid,
crate::disc::Stream::Subtitle(s) => s.pid,
}).collect()
}
+59 -191
View File
@@ -1,37 +1,31 @@
//! Stream URL resolver — parses URL strings into IOStream instances.
//! Stream URL resolver — parses URL strings into PES stream instances.
//!
//! Format: `scheme://path`
//!
//! | Scheme | Input | Output | Path |
//! |--------|-------|--------|------|
//! | disc:// | Yes | -- | empty (auto-detect) or /dev/sgN |
//! | m2ts:// | Yes | Yes | file path (required) |
//! | iso:// | Yes | -- | file path (required) |
//! | mkv:// | Yes | Yes | file path (required) |
//! | m2ts:// | Yes | Yes | file path (required) |
//! | network:// | Yes (listen) | Yes (connect) | host:port (required) |
//! | stdio:// | Yes (stdin) | Yes (stdout) | empty |
//! | iso:// | Yes | -- | file path (required) |
//! | null:// | -- | Yes | empty |
//!
//! Bare paths without a scheme are rejected.
//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
use super::disc::DiscStream;
use super::iso::IsoStream;
use super::network::NetworkStream;
use super::null::NullStream;
use super::stdio::StdioStream;
use super::{IOStream, M2tsStream, MkvStream};
use crate::disc::DiscTitle;
use std::io::{self, BufReader, BufWriter};
use super::{M2tsStream, MkvStream};
use std::io;
use std::path::{Path, PathBuf};
/// I/O buffer size for file streams.
const IO_BUF_SIZE: usize = 4 * 1024 * 1024;
/// Default MKV lookahead buffer size.
/// Dynamically increased for UHD content (many streams delay video codec headers).
const MKV_LOOKAHEAD_DEFAULT: usize = 10 * 1024 * 1024;
const MKV_LOOKAHEAD_UHD: usize = 100 * 1024 * 1024;
/// Parsed stream URL.
pub enum StreamUrl {
/// Optical disc drive. Device path is optional (auto-detect if None).
@@ -88,17 +82,6 @@ impl StreamUrl {
}
/// Parse a URL string into a typed StreamUrl.
///
/// All URLs must use the `scheme://path` format. Bare paths are not supported.
///
/// ```text
/// disc:// → Disc { device: None }
/// disc:///dev/sg4 → Disc { device: Some("/dev/sg4") }
/// m2ts:///tmp/Dune.m2ts → M2ts { path: "/tmp/Dune.m2ts" }
/// mkv://Dune.mkv → Mkv { path: "Dune.mkv" }
/// network://10.0.0.1:9000 → Network { addr: "10.0.0.1:9000" }
/// null:// → Null
/// ```
pub fn parse_url(url: &str) -> StreamUrl {
if let Some(rest) = url.strip_prefix("disc://") {
return if rest.is_empty() {
@@ -143,19 +126,14 @@ pub fn parse_url(url: &str) -> StreamUrl {
/// Validate that a file path is non-empty and has a filename component.
fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
if path.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{scheme}:// requires a file path (e.g. {scheme}://movie.{scheme})"),
));
return Err(crate::error::Error::StreamUrlMissingPath {
scheme: scheme.to_string(),
}.into());
}
if path.file_name().is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{scheme}://{} is not a valid file path — must include a filename",
path.display()
),
));
return Err(crate::error::Error::StreamUrlInvalid {
url: format!("{scheme}://{}", path.display()),
}.into());
}
Ok(())
}
@@ -163,136 +141,18 @@ fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
/// Validate that a network address has host:port format.
fn validate_network_addr(addr: &str) -> io::Result<()> {
if addr.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"network:// requires host:port (e.g. network://0.0.0.0:9000)",
));
return Err(crate::error::Error::StreamUrlMissingPath {
scheme: "network".to_string(),
}.into());
}
if !addr.contains(':') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("network://{addr} missing port — use network://{addr}:PORT"),
));
return Err(crate::error::Error::StreamUrlMissingPort {
addr: addr.to_string(),
}.into());
}
Ok(())
}
/// Open a stream URL for reading (source).
pub fn open_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn IOStream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Disc { device } => {
let result = DiscStream::open(
device.as_deref(),
opts.keydb_path.as_deref(),
opts.title_index.unwrap_or(0),
None,
)
.map_err(|e| io::Error::other(e.to_string()))?;
let mut stream = result.stream;
if opts.raw {
stream.set_raw();
}
Ok(Box::new(stream))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
let file = std::fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(),
format!("m2ts://{}: {}", path.display(), e)))?;
let reader = BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(M2tsStream::open(reader)?))
}
StreamUrl::Mkv { ref path } => {
validate_file_path(path, "mkv")?;
let file = std::fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(),
format!("mkv://{}: {}", path.display(), e)))?;
let reader = BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(MkvStream::open(reader)?))
}
StreamUrl::Network { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported,
"network:// requires PES pipeline — use input() instead of open_input()"))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::input()))
}
StreamUrl::Iso { ref path } => {
validate_file_path(path, "iso")?;
let scan_opts = match &opts.keydb_path {
Some(p) => crate::disc::ScanOptions::with_keydb(p),
None => crate::disc::ScanOptions::default(),
};
let mut stream = IsoStream::open(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
if opts.raw {
stream.set_raw();
}
Ok(Box::new(stream))
}
StreamUrl::Null => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
"null:// is write-only — cannot use as input"))
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL — use scheme://path (e.g. mkv://movie.mkv, disc://, m2ts://movie.m2ts)", raw)))
}
}
}
/// Open a stream URL for writing (destination).
pub fn open_output(url: &str, meta: &DiscTitle) -> io::Result<Box<dyn IOStream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Disc { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported,
"disc:// is read-only — cannot use as output"))
}
StreamUrl::Iso { ref path } => {
validate_file_path(path, "iso")?;
Ok(Box::new(IsoStream::create(&path.to_string_lossy())?.meta(meta)))
}
StreamUrl::Null => {
Ok(Box::new(NullStream::new().meta(meta)))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::output().meta(meta)))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(),
format!("m2ts://{}: {}", path.display(), e)))?;
let writer = BufWriter::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(M2tsStream::new(writer).meta(meta)))
}
StreamUrl::Mkv { ref path } => {
validate_file_path(path, "mkv")?;
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(),
format!("mkv://{}: {}", path.display(), e)))?;
let writer = BufWriter::with_capacity(IO_BUF_SIZE, file);
let lookahead = if meta.streams.len() > 15 {
MKV_LOOKAHEAD_UHD
} else {
MKV_LOOKAHEAD_DEFAULT
};
Ok(Box::new(MkvStream::new(writer).meta(meta).max_buffer(lookahead)))
}
StreamUrl::Network { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported,
"network:// output requires PES pipeline — use pipe() instead of open_output()"))
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL — use scheme://path (e.g. mkv://movie.mkv, m2ts://movie.m2ts, null://)", raw)))
}
}
}
/// Options for opening an input stream.
#[derive(Default)]
pub struct InputOptions {
@@ -302,42 +162,43 @@ pub struct InputOptions {
pub raw: bool,
}
// ── PES-based open ──────────────────────────────────────────────────────────
/// Open a PES input stream (produces PES frames).
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::Iso { ref path } => {
validate_file_path(path, "iso")?;
let scan_opts = match &opts.keydb_path {
Some(p) => crate::disc::ScanOptions::with_keydb(p),
None => crate::disc::ScanOptions::default(),
};
let mut stream = IsoStream::open(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
let mut stream = DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
if opts.raw {
stream.set_raw();
}
Ok(Box::new(stream))
}
StreamUrl::Disc { device } => {
let result = DiscStream::open(
device.as_deref(),
opts.keydb_path.as_deref(),
opts.title_index.unwrap_or(0),
None,
)
.map_err(|e| io::Error::other(e.to_string()))?;
let mut stream = result.stream;
if opts.raw {
stream.set_raw();
}
Ok(Box::new(stream))
}
StreamUrl::Null => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
"null:// is write-only — cannot use as input"))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
let file = std::fs::File::open(path)
@@ -359,9 +220,13 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::input()))
}
StreamUrl::Null => {
Err(crate::error::Error::StreamWriteOnly.into())
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL — use scheme://path (e.g. mkv://movie.mkv, disc://, m2ts://movie.m2ts)", raw)))
Err(crate::error::Error::StreamUrlInvalid {
url: raw.clone(),
}.into())
}
}
}
@@ -379,32 +244,35 @@ pub fn output(
.map_err(|e| io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e)))?;
let writer: Box<dyn super::WriteSeek> =
Box::new(std::io::BufWriter::with_capacity(IO_BUF_SIZE, file));
Ok(Box::new(super::mkvout::MkvOutputStream::create(writer, title)?))
Ok(Box::new(MkvStream::create(writer, title)?))
}
StreamUrl::M2ts { ref path } => {
validate_file_path(path, "m2ts")?;
Ok(Box::new(super::pesout::M2tsOutputStream::create(&path.to_string_lossy(), title)?))
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e)))?;
let writer = std::io::BufWriter::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(M2tsStream::create(writer, title)?))
}
StreamUrl::Network { ref addr } => {
validate_network_addr(addr)?;
Ok(Box::new(super::pesout::NetworkOutputStream::connect(addr, title)?))
Ok(Box::new(NetworkStream::connect(addr)?.meta(title)))
}
StreamUrl::Stdio => {
Ok(Box::new(super::pesout::StdioOutputStream::new(title)))
Ok(Box::new(StdioStream::output(title)))
}
StreamUrl::Null => {
Ok(Box::new(super::pesout::NullOutputStream::new(title)))
Ok(Box::new(NullStream::new(title)))
}
StreamUrl::Disc { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported, "disc:// is read-only"))
Err(crate::error::Error::StreamReadOnly.into())
}
StreamUrl::Iso { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported,
"ISO output from PES not supported — use disc.copy() for raw ISO"))
Err(crate::error::Error::StreamReadOnly.into())
}
StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' is not a valid stream URL", raw)))
Err(crate::error::Error::StreamUrlInvalid {
url: raw.clone(),
}.into())
}
}
}
+9 -92
View File
@@ -1,17 +1,13 @@
//! StdioStream — raw byte pipe via stdin/stdout. Format-agnostic.
//! StdioStream — PES frames via stdin/stdout.
use super::IOStream;
use crate::disc::DiscTitle;
use std::io::{self, Read, Write};
use std::io::{self, Write};
/// Stdio stream — reads from stdin, writes to stdout.
///
/// No headers, no metadata, no format opinions. Just bytes.
/// The format is determined by whatever is on the other end.
/// Stdio stream — reads PES from stdin, writes PES to stdout.
pub struct StdioStream {
disc_title: DiscTitle,
reader: Option<io::Stdin>,
writer: Option<io::Stdout>,
writer: Option<io::BufWriter<io::Stdout>>,
}
impl StdioStream {
@@ -25,32 +21,26 @@ impl StdioStream {
}
/// Create a stdio stream for writing (stdout).
pub fn output() -> Self {
pub fn output(title: &DiscTitle) -> Self {
Self {
disc_title: DiscTitle::empty(),
disc_title: title.clone(),
reader: None,
writer: Some(io::stdout()),
writer: Some(io::BufWriter::new(io::stdout())),
}
}
/// Set metadata (for output — passed through from input side).
pub fn meta(mut self, dt: &DiscTitle) -> Self {
self.disc_title = dt.clone();
self
}
}
impl crate::pes::Stream for StdioStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
match &mut self.reader {
Some(r) => crate::pes::PesFrame::deserialize(r),
None => Err(io::Error::new(io::ErrorKind::Unsupported, "stdio opened for writing")),
None => Err(crate::error::Error::StreamWriteOnly.into()),
}
}
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
match &mut self.writer {
Some(w) => frame.serialize(w),
None => Err(io::Error::new(io::ErrorKind::Unsupported, "stdio opened for reading")),
None => Err(crate::error::Error::StreamReadOnly.into()),
}
}
fn finish(&mut self) -> io::Result<()> {
@@ -59,76 +49,3 @@ impl crate::pes::Stream for StdioStream {
}
fn info(&self) -> &DiscTitle { &self.disc_title }
}
impl IOStream for StdioStream {
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if let Some(ref mut w) = self.writer {
w.flush()?;
}
Ok(())
}
}
impl Read for StdioStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.reader {
Some(ref mut r) => r.read(buf),
None => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stdio:// opened for output — cannot read",
)),
}
}
}
impl Write for StdioStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.writer {
Some(ref mut w) => w.write(buf),
None => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stdio:// opened for input — cannot write",
)),
}
}
fn flush(&mut self) -> io::Result<()> {
match self.writer {
Some(ref mut w) => w.flush(),
None => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
#[test]
fn stdio_output_write_errors_on_read() {
let mut stream = StdioStream::output();
let mut buf = [0u8; 10];
let err = stream.read(&mut buf).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
assert!(err.to_string().contains("cannot read"), "got: {}", err);
}
#[test]
fn stdio_input_read_errors_on_write() {
let mut stream = StdioStream::input();
let err = stream.write(&[0u8; 10]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
assert!(err.to_string().contains("cannot write"), "got: {}", err);
}
#[test]
fn stdio_total_bytes_returns_none() {
let input = StdioStream::input();
assert_eq!(input.total_bytes(), None);
let output = StdioStream::output();
assert_eq!(output.total_bytes(), None);
}
}