Fix batch overflow in Disc::copy(), DVD PGC parsing, demuxer flush at EOF
- Disc::copy() hardcoded batch=64 sectors, exceeding BU40N's 60-sector hw limit. Now accepts batch_sectors param, defaults to 60. - IFO PGC: playback time at offset 0x04 not 0x02, cell time at cell+4 - DiscStream: set demuxer from content_format (TS for BD, PS for DVD) - Flush TS/PS demuxers at EOF to avoid losing last PES frame - M2tsStream: flush demuxer at EOF - StdioStream: FMKV metadata header for roundtrip compatibility
This commit is contained in:
+3
-1
@@ -1113,6 +1113,7 @@ impl Disc {
|
|||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
decrypt: bool,
|
decrypt: bool,
|
||||||
resume: bool,
|
resume: bool,
|
||||||
|
batch_sectors: Option<u16>,
|
||||||
on_progress: Option<&dyn Fn(u64, u64)>,
|
on_progress: Option<&dyn Fn(u64, u64)>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
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 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 lba = start_lba;
|
||||||
let mut bytes_done = start_lba as u64 * 2048;
|
let mut bytes_done = start_lba as u64 * 2048;
|
||||||
let mut buf = vec![0u8; batch as usize * 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 count = remaining.min(batch as u32) as u16;
|
||||||
let bytes = count as usize * 2048;
|
let bytes = count as usize * 2048;
|
||||||
|
|
||||||
|
|
||||||
reader
|
reader
|
||||||
.read_sectors(lba, count, &mut buf[..bytes])
|
.read_sectors(lba, count, &mut buf[..bytes])
|
||||||
.map_err(|e| Error::IoError {
|
.map_err(|e| Error::IoError {
|
||||||
|
|||||||
+51
-17
@@ -488,26 +488,20 @@ fn parse_pgcit(
|
|||||||
|
|
||||||
/// Parse a single PGC (Program Chain) to extract duration and cells.
|
/// Parse a single PGC (Program Chain) to extract duration and cells.
|
||||||
fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result<DvdTitle> {
|
fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result<DvdTitle> {
|
||||||
// PGC needs at least 0xE6 bytes for the cell info offsets
|
// PGC needs at least 0xE8 bytes for the cell playback info offset
|
||||||
if pgc_offset + 0xE6 > data.len() {
|
if pgc_offset + 0xEA > data.len() {
|
||||||
return Err(Error::IfoParse);
|
return Err(Error::IfoParse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playback time at offset 2-5 (4 BCD bytes)
|
// PGC layout:
|
||||||
let time_bytes = sub_slice(data, pgc_offset + 2, 4)?;
|
// 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);
|
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)
|
// Cell playback info table offset (relative to PGC start)
|
||||||
let cell_playback_offset = be_u16(data, pgc_offset + 0xE8)? as usize;
|
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<DvdTitle>
|
|||||||
let cell_base = pgc_offset + cell_playback_offset;
|
let cell_base = pgc_offset + cell_playback_offset;
|
||||||
let mut total = 0.0;
|
let mut total = 0.0;
|
||||||
for i in 0..cells.len() {
|
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;
|
let co = cell_base + i * 24;
|
||||||
if co + 4 <= data.len() {
|
if co + 8 <= data.len() {
|
||||||
total += bcd_to_secs(&data[co..co + 4]);
|
total += bcd_to_secs(&data[co + 4..co + 8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
total
|
total
|
||||||
@@ -695,6 +690,45 @@ mod tests {
|
|||||||
assert_eq!(info.title_sets.len(), 1);
|
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]
|
#[test]
|
||||||
fn video_attr_parsing() {
|
fn video_attr_parsing() {
|
||||||
// Build minimal data with video attrs at 0x200
|
// Build minimal data with video attrs at 0x200
|
||||||
|
|||||||
+60
-4
@@ -75,10 +75,18 @@ impl DiscStream {
|
|||||||
|
|
||||||
let mut stream = Self::from_reader(Box::new(drive), title, keys, max_batch);
|
let mut stream = Self::from_reader(Box::new(drive), title, keys, max_batch);
|
||||||
|
|
||||||
if content_format == crate::disc::ContentFormat::MpegPs {
|
// Set demuxer based on content format
|
||||||
stream.ts_demuxer = None;
|
match content_format {
|
||||||
|
crate::disc::ContentFormat::MpegPs => {
|
||||||
stream.ps_demuxer = Some(super::ps::PsDemuxer::new());
|
stream.ps_demuxer = Some(super::ps::PsDemuxer::new());
|
||||||
}
|
}
|
||||||
|
crate::disc::ContentFormat::BdTs => {
|
||||||
|
let pids: Vec<u16> = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect();
|
||||||
|
if !pids.is_empty() {
|
||||||
|
stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok((stream, disc))
|
Ok((stream, disc))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +118,18 @@ impl DiscStream {
|
|||||||
let batch: u16 = 64;
|
let batch: u16 = 64;
|
||||||
|
|
||||||
let mut stream = Self::from_reader(Box::new(reader), title, keys, batch);
|
let mut stream = Self::from_reader(Box::new(reader), title, keys, batch);
|
||||||
|
// Set demuxer based on content format
|
||||||
|
match disc.content_format {
|
||||||
|
crate::disc::ContentFormat::MpegPs => {
|
||||||
|
stream.ps_demuxer = Some(super::ps::PsDemuxer::new());
|
||||||
|
}
|
||||||
|
crate::disc::ContentFormat::BdTs => {
|
||||||
|
let pids: Vec<u16> = stream.pid_to_track.iter().map(|(pid, _)| *pid).collect();
|
||||||
|
if !pids.is_empty() {
|
||||||
|
stream.ts_demuxer = Some(super::ts::TsDemuxer::new(&pids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
stream.disc = Some(disc);
|
stream.disc = Some(disc);
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
@@ -150,7 +170,9 @@ impl DiscStream {
|
|||||||
batch_sectors,
|
batch_sectors,
|
||||||
errors: 0,
|
errors: 0,
|
||||||
eof: false,
|
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,
|
ps_demuxer: None,
|
||||||
parsers,
|
parsers,
|
||||||
pending_frames: std::collections::VecDeque::new(),
|
pending_frames: std::collections::VecDeque::new(),
|
||||||
@@ -216,7 +238,41 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
loop {
|
loop {
|
||||||
if !self.fill_extents() {
|
if !self.fill_extents() {
|
||||||
self.eof = true;
|
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;
|
let bytes = self.buf_valid;
|
||||||
|
|||||||
+15
-1
@@ -172,7 +172,21 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
let n = reader.read(&mut buf)?;
|
let n = reader.read(&mut buf)?;
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
self.pes_eof = true;
|
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 {
|
if let Some(ref mut demuxer) = self.demuxer {
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ impl PsDemuxer {
|
|||||||
|
|
||||||
packets
|
packets
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether a start code byte is a valid PES stream ID that carries payload.
|
/// Check whether a start code byte is a valid PES stream ID that carries payload.
|
||||||
|
|||||||
+51
-2
@@ -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 crate::disc::DiscTitle;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
/// Stdio stream — reads PES from stdin, writes PES to stdout.
|
/// Stdio stream — reads PES from stdin, writes PES to stdout.
|
||||||
|
/// FMKV metadata header is written/read automatically.
|
||||||
pub struct StdioStream {
|
pub struct StdioStream {
|
||||||
disc_title: DiscTitle,
|
disc_title: DiscTitle,
|
||||||
reader: Option<io::Stdin>,
|
reader: Option<io::Stdin>,
|
||||||
writer: Option<io::BufWriter<io::Stdout>>,
|
writer: Option<io::BufWriter<io::Stdout>>,
|
||||||
|
header_written: bool,
|
||||||
|
header_read: bool,
|
||||||
|
stored_codec_privates: Vec<Option<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdioStream {
|
impl StdioStream {
|
||||||
@@ -17,6 +25,9 @@ impl StdioStream {
|
|||||||
disc_title: DiscTitle::empty(),
|
disc_title: DiscTitle::empty(),
|
||||||
reader: Some(io::stdin()),
|
reader: Some(io::stdin()),
|
||||||
writer: None,
|
writer: None,
|
||||||
|
header_written: false,
|
||||||
|
header_read: false,
|
||||||
|
stored_codec_privates: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +37,32 @@ impl StdioStream {
|
|||||||
disc_title: title.clone(),
|
disc_title: title.clone(),
|
||||||
reader: None,
|
reader: None,
|
||||||
writer: Some(io::BufWriter::new(io::stdout())),
|
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 {
|
impl crate::pes::Stream for StdioStream {
|
||||||
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
|
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
|
||||||
|
self.ensure_header_read()?;
|
||||||
match &mut self.reader {
|
match &mut self.reader {
|
||||||
Some(r) => crate::pes::PesFrame::deserialize(r),
|
Some(r) => crate::pes::PesFrame::deserialize(r),
|
||||||
None => Err(crate::error::Error::StreamWriteOnly.into()),
|
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<()> {
|
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||||
match &mut self.writer {
|
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()),
|
None => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,4 +88,13 @@ impl crate::pes::Stream for StdioStream {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
fn info(&self) -> &DiscTitle { &self.disc_title }
|
||||||
|
|
||||||
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user