diff --git a/src/disc/mod.rs b/src/disc/mod.rs index fdbc24b..85db586 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -1113,6 +1113,7 @@ impl Disc { path: &std::path::Path, decrypt: bool, resume: bool, + batch_sectors: Option, on_progress: Option<&dyn Fn(u64, u64)>, ) -> Result<()> { use std::io::{Seek, SeekFrom, Write}; @@ -1149,7 +1150,7 @@ impl Disc { }; let mut writer = std::io::BufWriter::with_capacity(4 * 1024 * 1024, file); - let batch: u16 = 64; // 128 KB per read + let batch: u16 = batch_sectors.unwrap_or(DEFAULT_BATCH_SECTORS); let mut lba = start_lba; let mut bytes_done = start_lba as u64 * 2048; let mut buf = vec![0u8; batch as usize * 2048]; @@ -1159,6 +1160,7 @@ impl Disc { let count = remaining.min(batch as u32) as u16; let bytes = count as usize * 2048; + reader .read_sectors(lba, count, &mut buf[..bytes]) .map_err(|e| Error::IoError { diff --git a/src/ifo.rs b/src/ifo.rs index 29b20c2..87399d0 100644 --- a/src/ifo.rs +++ b/src/ifo.rs @@ -488,26 +488,20 @@ fn parse_pgcit( /// Parse a single PGC (Program Chain) to extract duration and cells. fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result { - // PGC needs at least 0xE6 bytes for the cell info offsets - if pgc_offset + 0xE6 > data.len() { + // PGC needs at least 0xE8 bytes for the cell playback info offset + if pgc_offset + 0xEA > data.len() { return Err(Error::IfoParse); } - // Playback time at offset 2-5 (4 BCD bytes) - let time_bytes = sub_slice(data, pgc_offset + 2, 4)?; + // PGC layout: + // 0x00-0x01: misc flags + // 0x02: nr_of_programs + // 0x03: nr_of_cells + // 0x04-0x07: playback_time (4 BCD bytes) + let num_cells = byte_at(data, pgc_offset + 0x03)? as usize; + let time_bytes = sub_slice(data, pgc_offset + 0x04, 4)?; let duration_secs = bcd_to_secs(time_bytes); - // Number of cells: the user spec says byte 0x03, but in the standard IFO - // format bytes 0x02-0x05 are the BCD playback time. The real cell count - // lives at PGC offset 0x07. We read from 0x03 as primary (per spec) and - // fall back to 0x07 if that yields zero. - let num_cells_primary = byte_at(data, pgc_offset + 0x03)? as usize; - let num_cells = if num_cells_primary == 0 { - byte_at(data, pgc_offset + 0x07).unwrap_or(0) as usize - } else { - num_cells_primary - }; - // Cell playback info table offset (relative to PGC start) let cell_playback_offset = be_u16(data, pgc_offset + 0xE8)? as usize; @@ -536,9 +530,10 @@ fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result let cell_base = pgc_offset + cell_playback_offset; let mut total = 0.0; for i in 0..cells.len() { + // Cell playback info: 24 bytes per cell, BCD time at offset 4-7 let co = cell_base + i * 24; - if co + 4 <= data.len() { - total += bcd_to_secs(&data[co..co + 4]); + if co + 8 <= data.len() { + total += bcd_to_secs(&data[co + 4..co + 8]); } } total @@ -695,6 +690,45 @@ mod tests { assert_eq!(info.title_sets.len(), 1); } + #[test] + fn pgc_parses_duration_from_correct_offset() { + // Build a minimal PGC: 0xEA bytes minimum + // PGC layout: 0x02 = nr_programs, 0x03 = nr_cells, 0x04-0x07 = BCD time + let mut pgc = vec![0u8; 0xEA]; + pgc[0x02] = 1; // 1 program + pgc[0x03] = 2; // 2 cells + // 1h 59m 30s at 29.97fps, 0 frames + pgc[0x04] = 0x01; // hours BCD + pgc[0x05] = 0x59; // minutes BCD + pgc[0x06] = 0x30; // seconds BCD + pgc[0x07] = 0b11_000000; // 29.97fps, 0 frames + // Cell playback info offset at PGC+0xE8 + let cell_offset: u16 = 0xEA; // right after minimum header + pgc[0xE8] = (cell_offset >> 8) as u8; + pgc[0xE9] = cell_offset as u8; + // Add 2 cells (24 bytes each) + pgc.resize(pgc.len() + 48, 0); + // Cell 0: sectors 100-200 + let co = 0xEA; + pgc[co + 8] = 0; pgc[co + 9] = 0; pgc[co + 10] = 0; pgc[co + 11] = 100; // first sector + pgc[co + 20] = 0; pgc[co + 21] = 0; pgc[co + 22] = 0; pgc[co + 23] = 200; // last sector + // Cell 1: sectors 300-400 + let co = 0xEA + 24; + pgc[co + 8] = 0; pgc[co + 9] = 0; pgc[co + 10] = 1; pgc[co + 11] = 44; // first sector = 300 + pgc[co + 20] = 0; pgc[co + 21] = 0; pgc[co + 22] = 1; pgc[co + 23] = 144; // last sector = 400 + + let title = parse_pgc(&pgc, 0, 5).unwrap(); + let expected = 1.0 * 3600.0 + 59.0 * 60.0 + 30.0; + assert!((title.duration_secs - expected).abs() < 0.1, + "expected ~{expected}s, got {}s", title.duration_secs); + assert_eq!(title.chapters, 5); + assert_eq!(title.cells.len(), 2); + assert_eq!(title.cells[0].first_sector, 100); + assert_eq!(title.cells[0].last_sector, 200); + assert_eq!(title.cells[1].first_sector, 300); + assert_eq!(title.cells[1].last_sector, 400); + } + #[test] fn video_attr_parsing() { // Build minimal data with video attrs at 0x200 diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 7bbe534..36534da 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -75,9 +75,17 @@ impl DiscStream { 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()); + // Set demuxer based on content format + match content_format { + crate::disc::ContentFormat::MpegPs => { + stream.ps_demuxer = Some(super::ps::PsDemuxer::new()); + } + crate::disc::ContentFormat::BdTs => { + let pids: Vec = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect(); + if !pids.is_empty() { + stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids)); + } + } } Ok((stream, disc)) } @@ -110,6 +118,18 @@ impl DiscStream { let batch: u16 = 64; let mut stream = Self::from_reader(Box::new(reader), title, keys, batch); + // Set demuxer based on content format + match disc.content_format { + crate::disc::ContentFormat::MpegPs => { + stream.ps_demuxer = Some(super::ps::PsDemuxer::new()); + } + crate::disc::ContentFormat::BdTs => { + let pids: Vec = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect(); + if !pids.is_empty() { + stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids)); + } + } + } stream.disc = Some(disc); Ok(stream) } @@ -150,7 +170,9 @@ impl DiscStream { batch_sectors, errors: 0, eof: false, - ts_demuxer: if pids.is_empty() { None } else { Some(super::ts::TsDemuxer::new(&pids)) }, + // Demuxer set by caller — open_drive() checks content_format, + // open_iso() always uses TS (Blu-ray ISO). + ts_demuxer: None, ps_demuxer: None, parsers, pending_frames: std::collections::VecDeque::new(), @@ -216,7 +238,41 @@ impl crate::pes::Stream for DiscStream { loop { if !self.fill_extents() { self.eof = true; - return Ok(None); + // Flush demuxer — last PES packet may still be in the assembler + if let Some(ref mut demuxer) = self.ts_demuxer { + for pes in &demuxer.flush() { + if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) { + 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, frame) + ); + } + } + } + } + } + // PS demuxer flush (DVD) + if let Some(ref mut demuxer) = self.ps_demuxer { + for ps in &demuxer.flush() { + let track = match ps.stream_id { + 0xE0..=0xEF => 0, + 0xC0..=0xDF => 1, + 0xBD => ps.sub_stream_id.map(|s| (s & 0x1F) as usize + 1).unwrap_or(1), + _ => continue, + }; + if track < self.title.streams.len() { + let pts_ns = ps.pts.map(|p| (p as i64) * 1_000_000_000 / 90_000).unwrap_or(0); + self.pending_frames.push_back(crate::pes::PesFrame { + track, + pts: pts_ns, + keyframe: true, + data: ps.data.clone(), + }); + } + } + } + return Ok(self.pending_frames.pop_front()); } let bytes = self.buf_valid; diff --git a/src/mux/m2ts.rs b/src/mux/m2ts.rs index 517666e..5371ae9 100644 --- a/src/mux/m2ts.rs +++ b/src/mux/m2ts.rs @@ -172,7 +172,21 @@ impl crate::pes::Stream for M2tsStream { let n = reader.read(&mut buf)?; if n == 0 { self.pes_eof = true; - return Ok(None); + // Flush demuxer — last PES packet may still be in the assembler + if let Some(ref mut demuxer) = self.demuxer { + for pes in &demuxer.flush() { + if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) { + 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, frame) + ); + } + } + } + } + } + return Ok(self.pending_frames.pop_front()); } if let Some(ref mut demuxer) = self.demuxer { diff --git a/src/mux/ps.rs b/src/mux/ps.rs index 0ea5c97..761b7b6 100644 --- a/src/mux/ps.rs +++ b/src/mux/ps.rs @@ -163,6 +163,7 @@ impl PsDemuxer { packets } + } /// Check whether a start code byte is a valid PES stream ID that carries payload. diff --git a/src/mux/stdio.rs b/src/mux/stdio.rs index 700b02a..d7d7f0f 100644 --- a/src/mux/stdio.rs +++ b/src/mux/stdio.rs @@ -1,13 +1,21 @@ -//! StdioStream — PES frames via stdin/stdout. +//! StdioStream — PES frames via stdin/stdout with FMKV metadata header. +//! +//! The FMKV header carries stream metadata (PIDs, codecs, languages, codec_privates) +//! so the receiving end can set up muxing without scanning the content. +use super::meta; use crate::disc::DiscTitle; use std::io::{self, Write}; /// Stdio stream — reads PES from stdin, writes PES to stdout. +/// FMKV metadata header is written/read automatically. pub struct StdioStream { disc_title: DiscTitle, reader: Option, writer: Option>, + header_written: bool, + header_read: bool, + stored_codec_privates: Vec>>, } impl StdioStream { @@ -17,6 +25,9 @@ impl StdioStream { disc_title: DiscTitle::empty(), reader: Some(io::stdin()), writer: None, + header_written: false, + header_read: false, + stored_codec_privates: Vec::new(), } } @@ -26,12 +37,32 @@ impl StdioStream { disc_title: title.clone(), reader: None, writer: Some(io::BufWriter::new(io::stdout())), + header_written: false, + header_read: false, + stored_codec_privates: Vec::new(), } } + + /// Read the FMKV metadata header from stdin on first read. + fn ensure_header_read(&mut self) -> io::Result<()> { + if self.header_read { + return Ok(()); + } + self.header_read = true; + if let Some(ref mut r) = self.reader { + if let Ok(Some(m)) = meta::read_header(r) { + let title = m.to_title(); + self.stored_codec_privates = title.codec_privates.clone(); + self.disc_title = title; + } + } + Ok(()) + } } impl crate::pes::Stream for StdioStream { fn read(&mut self) -> io::Result> { + self.ensure_header_read()?; match &mut self.reader { Some(r) => crate::pes::PesFrame::deserialize(r), None => Err(crate::error::Error::StreamWriteOnly.into()), @@ -39,7 +70,16 @@ impl crate::pes::Stream for StdioStream { } fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> { match &mut self.writer { - Some(w) => frame.serialize(w), + Some(ref mut w) => { + if !self.header_written { + if !self.disc_title.streams.is_empty() { + let m = meta::M2tsMeta::from_title(&self.disc_title); + meta::write_header(w, &m)?; + } + self.header_written = true; + } + frame.serialize(w) + } None => Err(crate::error::Error::StreamReadOnly.into()), } } @@ -48,4 +88,13 @@ impl crate::pes::Stream for StdioStream { Ok(()) } fn info(&self) -> &DiscTitle { &self.disc_title } + + fn codec_private(&self, track: usize) -> Option> { + self.stored_codec_privates.get(track).and_then(|c| c.clone()) + } + + fn headers_ready(&self) -> bool { + // After first read(), header is parsed and codec_privates populated + self.header_read || self.writer.is_some() + } }