Fix cargo fmt formatting

This commit is contained in:
MattJackson
2026-04-15 22:32:53 +00:00
parent d06a37d3dc
commit e0f40583c4
29 changed files with 586 additions and 240 deletions
+15 -4
View File
@@ -34,8 +34,10 @@ fn main() {
let file = std::fs::File::create("/dev/null").unwrap();
let mut writer = std::io::BufWriter::with_capacity(4 * 1024 * 1024, file);
eprintln!("Reading 1000 batches ({:.1} MB) with write + progress...",
1000.0 * batch as f64 * 2048.0 / 1_048_576.0);
eprintln!(
"Reading 1000 batches ({:.1} MB) with write + progress...",
1000.0 * batch as f64 * 2048.0 / 1_048_576.0
);
let start = Instant::now();
let mut ok = 0u32;
@@ -51,7 +53,9 @@ fn main() {
}
Err(e) => {
fail += 1;
if fail <= 5 { eprintln!(" FAIL LBA {}: {}", lba, e); }
if fail <= 5 {
eprintln!(" FAIL LBA {}: {}", lba, e);
}
buf.fill(0);
writer.write_all(&buf).unwrap();
}
@@ -67,5 +71,12 @@ fn main() {
let elapsed = start.elapsed().as_secs_f64();
let mb = ok as f64 * batch as f64 * 2048.0 / 1_048_576.0;
eprintln!("\n{} ok, {} fail, {:.1} MB in {:.1}s = {:.1} MB/s", ok, fail, mb, elapsed, mb / elapsed);
eprintln!(
"\n{} ok, {} fail, {:.1} MB in {:.1}s = {:.1} MB/s",
ok,
fail,
mb,
elapsed,
mb / elapsed
);
}
+16 -3
View File
@@ -50,7 +50,9 @@ fn main() {
eprintln!("\n SLOW READ: LBA {} took {}ms (ok={})", lba, read_ms, ok);
}
if !ok { buf[..n].fill(0); }
if !ok {
buf[..n].fill(0);
}
w.write_all(&buf[..n]).unwrap();
lba += count as u32;
bytes += n as u64;
@@ -60,11 +62,22 @@ fn main() {
let speed = delta as f64 / last.elapsed().as_secs_f64() / 1_048_576.0;
let avg = bytes as f64 / start.elapsed().as_secs_f64() / 1_048_576.0;
let pct = bytes as f64 / (cap as f64 * 2048.0) * 100.0;
eprint!("\r {:.1}% LBA {} | {:.0} MB/s (avg {:.0}) | {:.1} GB ", pct, lba, speed, avg, bytes as f64 / 1e9);
eprint!(
"\r {:.1}% LBA {} | {:.0} MB/s (avg {:.0}) | {:.1} GB ",
pct,
lba,
speed,
avg,
bytes as f64 / 1e9
);
last_bytes = bytes;
last = Instant::now();
}
}
w.flush().unwrap();
eprintln!("\nDone: {:.1} GB in {:.0}s", bytes as f64 / 1e9, start.elapsed().as_secs_f64());
eprintln!(
"\nDone: {:.1} GB in {:.0}s",
bytes as f64 / 1e9,
start.elapsed().as_secs_f64()
);
}
+1 -1
View File
@@ -111,7 +111,7 @@ impl Disc {
chapters: Vec::new(),
extents,
content_format: ContentFormat::MpegPs,
codec_privates: Vec::new(),
codec_privates: Vec::new(),
});
}
}
+8 -6
View File
@@ -1119,7 +1119,11 @@ impl Disc {
use std::io::{Seek, SeekFrom, Write};
let total_bytes = self.capacity_sectors as u64 * 2048;
let keys = if decrypt { self.decrypt_keys() } else { crate::decrypt::DecryptKeys::None };
let keys = if decrypt {
self.decrypt_keys()
} else {
crate::decrypt::DecryptKeys::None
};
// Resume: check existing file
let (start_lba, file) = if resume {
@@ -1138,14 +1142,13 @@ impl Disc {
(safe_sectors, f)
}
_ => {
let f = std::fs::File::create(path)
.map_err(|e| Error::IoError { source: e })?;
let f =
std::fs::File::create(path).map_err(|e| Error::IoError { source: e })?;
(0u32, f)
}
}
} else {
let f = std::fs::File::create(path)
.map_err(|e| Error::IoError { source: e })?;
let f = std::fs::File::create(path).map_err(|e| Error::IoError { source: e })?;
(0u32, f)
};
@@ -1160,7 +1163,6 @@ 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 {
+69 -9
View File
@@ -150,7 +150,18 @@ impl Drive {
/// Uses GET EVENT STATUS NOTIFICATION which works regardless of firmware state.
pub fn drive_status(&mut self) -> DriveStatus {
// GET EVENT STATUS NOTIFICATION: polled, media event class (0x10)
let cdb = [SCSI_GET_EVENT_STATUS, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00];
let cdb = [
SCSI_GET_EVENT_STATUS,
0x01,
0x00,
0x00,
0x10,
0x00,
0x00,
0x00,
0x08,
0x00,
];
let mut buf = [0u8; 8];
match self.scsi.as_mut().execute(
&cdb,
@@ -343,7 +354,18 @@ impl Drive {
/// Read REPORT KEY RPC state (region playback control).
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
let cdb = [
SCSI_REPORT_KEY, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00,
SCSI_REPORT_KEY,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x08,
0x08,
0x00,
];
let mut buf = vec![0u8; 8];
let r = self
@@ -365,7 +387,18 @@ impl Drive {
/// Read MODE SENSE page data.
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
let cdb = [SCSI_MODE_SENSE, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00];
let cdb = [
SCSI_MODE_SENSE,
0x00,
page,
0x00,
0x00,
0x00,
0x00,
0x00,
0xFC,
0x00,
];
let mut buf = vec![0u8; 252];
let r = self
.scsi
@@ -421,7 +454,11 @@ impl Drive {
/// Returns Err only after all attempts exhausted — user should clean
/// the disc and resume.
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let timeout_ms = if self.recovery_bytes_remaining > 0 { 30_000 } else { 10_000 };
let timeout_ms = if self.recovery_bytes_remaining > 0 {
30_000
} else {
10_000
};
let cdb = [
crate::scsi::SCSI_READ_10,
0x00,
@@ -437,7 +474,10 @@ impl Drive {
// Normal read
if let Ok(result) = self.scsi.as_mut().execute(
&cdb, crate::scsi::DataDirection::FromDevice, buf, timeout_ms,
&cdb,
crate::scsi::DataDirection::FromDevice,
buf,
timeout_ms,
) {
if self.recovery_bytes_remaining > 0 {
let bytes_read = count as u64 * 2048;
@@ -457,7 +497,10 @@ impl Drive {
std::thread::sleep(std::time::Duration::from_secs(30));
if let Ok(result) = self.scsi.as_mut().execute(
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
&cdb,
crate::scsi::DataDirection::FromDevice,
buf,
30_000,
) {
self.recovery_bytes_remaining = RECOVERY_WINDOW;
return Ok(result.bytes_transferred);
@@ -479,7 +522,10 @@ impl Drive {
std::thread::sleep(std::time::Duration::from_secs(30));
if let Ok(result) = self.scsi.as_mut().execute(
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
&cdb,
crate::scsi::DataDirection::FromDevice,
buf,
30_000,
) {
self.recovery_bytes_remaining = RECOVERY_WINDOW;
return Ok(result.bytes_transferred);
@@ -524,7 +570,14 @@ impl Drive {
/// Lock the tray so the disc cannot be ejected during a rip.
pub fn lock_tray(&mut self) {
let prevent = [SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL, 0x00, 0x00, 0x00, 0x01, 0x00];
let prevent = [
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
0x00,
0x00,
0x00,
0x01,
0x00,
];
let mut buf = [0u8; 0];
let _ =
self.scsi
@@ -534,7 +587,14 @@ impl Drive {
/// Unlock the tray so the user can manually eject the disc.
pub fn unlock_tray(&mut self) {
let allow = [SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL, 0x00, 0x00, 0x00, 0x00, 0x00];
let allow = [
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
0x00,
0x00,
0x00,
0x00,
0x00,
];
let mut buf = [0u8; 0];
let _ =
self.scsi
+88 -26
View File
@@ -89,10 +89,18 @@ pub const E_NO_METADATA: u16 = 9008;
#[derive(Debug)]
pub enum Error {
// Device (1xxx)
DeviceNotFound { path: String },
DevicePermission { path: String },
DeviceNotReady { path: String },
DeviceResetFailed { path: String },
DeviceNotFound {
path: String,
},
DevicePermission {
path: String,
},
DeviceNotReady {
path: String,
},
DeviceResetFailed {
path: String,
},
// Profile (2xxx)
UnsupportedDrive {
@@ -104,7 +112,10 @@ pub enum Error {
// Unlock (3xxx)
UnlockFailed,
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
SignatureMismatch {
expected: [u8; 4],
got: [u8; 4],
},
// SCSI (4xxx)
ScsiError {
@@ -114,14 +125,23 @@ pub enum Error {
},
// I/O (5xxx)
IoError { source: std::io::Error },
IoError {
source: std::io::Error,
},
// Disc format (6xxx)
DiscRead { sector: u64 },
DiscRead {
sector: u64,
},
MplsParse,
ClpiParse,
UdfNotFound { path: String },
DiscTitleRange { index: usize, count: usize },
UdfNotFound {
path: String,
},
DiscTitleRange {
index: usize,
count: usize,
},
IfoParse,
MkvInvalid,
NoStreams,
@@ -142,22 +162,40 @@ pub enum Error {
DecryptFailed,
// Keydb (8xxx)
KeydbConnect { host: String },
KeydbHttp { status: u16 },
KeydbConnect {
host: String,
},
KeydbHttp {
status: u16,
},
KeydbInvalid,
KeydbWrite { path: String },
KeydbWrite {
path: String,
},
KeydbParse,
KeydbLoad { path: String },
KeydbLoad {
path: String,
},
// Stream/mux (9xxx)
StreamReadOnly,
StreamWriteOnly,
StreamUrlInvalid { url: String },
StreamUrlMissingPath { scheme: String },
StreamUrlMissingPort { addr: String },
PesFrameTooLarge { size: usize },
StreamUrlInvalid {
url: String,
},
StreamUrlMissingPath {
scheme: String,
},
StreamUrlMissingPort {
addr: String,
},
PesFrameTooLarge {
size: usize,
},
PesInvalidMagic,
IsoTooLarge { path: String },
IsoTooLarge {
path: String,
},
NoMetadata,
}
@@ -227,22 +265,46 @@ impl std::fmt::Display for Error {
product_id,
product_revision,
} => write!(
f, "E{}: {} {} {}",
self.code(), vendor_id.trim(), product_id.trim(), product_revision.trim()
f,
"E{}: {} {} {}",
self.code(),
vendor_id.trim(),
product_id.trim(),
product_revision.trim()
),
Error::SignatureMismatch { expected, got } => write!(
f, "E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
f,
"E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
self.code(),
expected[0], expected[1], expected[2], expected[3],
got[0], got[1], got[2], got[3]
expected[0],
expected[1],
expected[2],
expected[3],
got[0],
got[1],
got[2],
got[3]
),
Error::ScsiError { opcode, status, sense_key } => {
write!(f, "E{}: 0x{:02x}/0x{:02x}/0x{:02x}", self.code(), opcode, status, sense_key)
Error::ScsiError {
opcode,
status,
sense_key,
} => {
write!(
f,
"E{}: 0x{:02x}/0x{:02x}/0x{:02x}",
self.code(),
opcode,
status,
sense_key
)
}
Error::IoError { source } => write!(f, "E{}: {}", self.code(), source),
Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector),
Error::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path),
Error::DiscTitleRange { index, count } => write!(f, "E{}: {}/{}", self.code(), index, count),
Error::DiscTitleRange { index, count } => {
write!(f, "E{}: {}/{}", self.code(), index, count)
}
Error::KeydbConnect { host } => write!(f, "E{}: {}", self.code(), host),
Error::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status),
Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path),
-2
View File
@@ -26,7 +26,6 @@ pub struct Event {
#[derive(Debug)]
pub enum EventKind {
// ── Init sequence events ────────────────────────────────────────
/// Drive opened successfully.
DriveOpened { device: String },
@@ -43,7 +42,6 @@ pub enum EventKind {
ScanComplete { titles: usize },
// ── Read events ─────────────────────────────────────────────────
/// Bytes successfully read and written to output.
BytesRead {
/// Bytes written so far.
+24 -9
View File
@@ -697,12 +697,12 @@ mod tests {
let mut pgc = vec![0u8; 0xEA];
pgc[0x02] = 1; // 1 program
pgc[0x03] = 2; // 2 cells
// 1h 59m 30s at 29.97fps, 0 frames
// 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
// 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;
@@ -710,17 +710,32 @@ mod tests {
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
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
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!(
(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);
+2 -2
View File
@@ -104,8 +104,8 @@ pub use profile::DriveProfile;
// Platform trait is pub(crate) -- callers use Drive, not Platform directly
pub use decrypt::{decrypt_sectors, DecryptKeys};
pub use disc::{
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat,
Disc, DiscFormat, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate,
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, Disc,
DiscFormat, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate,
ScanOptions, Stream, SubtitleStream, VideoStream,
};
pub use mux::DiscStream;
+9 -3
View File
@@ -69,9 +69,15 @@ impl CodecParser for HevcParser {
let nal_type = (data[nal_start] >> 1) & 0x3F;
match nal_type {
NAL_VPS => { self.vps = Some(data[nal_start..end].to_vec()); }
NAL_SPS => { self.sps = Some(data[nal_start..end].to_vec()); }
NAL_PPS => { self.pps = Some(data[nal_start..end].to_vec()); }
NAL_VPS => {
self.vps = Some(data[nal_start..end].to_vec());
}
NAL_SPS => {
self.sps = Some(data[nal_start..end].to_vec());
}
NAL_PPS => {
self.pps = Some(data[nal_start..end].to_vec());
}
NAL_AUD => {} // Skip access unit delimiters
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
keyframe = true;
+53 -26
View File
@@ -5,9 +5,7 @@
//!
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
use crate::disc::{
detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions,
};
use crate::disc::{detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions};
use crate::sector::SectorReader;
use std::io;
@@ -99,8 +97,8 @@ impl DiscStream {
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() })?;
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());
@@ -110,7 +108,8 @@ impl DiscStream {
return Err(crate::error::Error::DiscTitleRange {
index: idx,
count: disc.titles.len(),
}.into());
}
.into());
}
let title = disc.titles[idx].clone();
@@ -210,7 +209,10 @@ impl DiscStream {
let bytes = sectors as usize * 2048;
self.read_buf.resize(bytes, 0);
match self.reader.read_sectors(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.current_offset += sectors as u32;
@@ -241,11 +243,15 @@ impl crate::pes::Stream for DiscStream {
// 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) {
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)
crate::pes::PesFrame::from_codec_frame(*track, frame),
);
}
}
@@ -258,11 +264,17 @@ impl crate::pes::Stream for DiscStream {
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),
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);
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,
@@ -276,22 +288,24 @@ impl crate::pes::Stream for DiscStream {
}
let bytes = self.buf_valid;
if let Err(e) = crate::decrypt::decrypt_sectors(
&mut self.read_buf[..bytes],
&self.decrypt_keys,
0,
) {
if let Err(e) =
crate::decrypt::decrypt_sectors(&mut self.read_buf[..bytes], &self.decrypt_keys, 0)
{
return Err(e.into());
}
if let Some(ref mut demuxer) = self.ts_demuxer {
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) {
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
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)
crate::pes::PesFrame::from_codec_frame(*track, frame),
);
}
}
@@ -303,11 +317,17 @@ impl crate::pes::Stream for DiscStream {
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),
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);
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,
@@ -330,15 +350,22 @@ impl crate::pes::Stream for DiscStream {
Err(crate::error::Error::StreamReadOnly.into())
}
fn finish(&mut self) -> io::Result<()> { 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()
let pid = self
.pid_to_track
.iter()
.find(|(_, idx)| *idx == track)
.map(|(pid, _)| *pid)?;
self.parsers.iter()
self.parsers
.iter()
.find(|(p, _)| *p == pid)
.and_then(|(_, parser)| parser.codec_private())
}
+2 -1
View File
@@ -26,7 +26,8 @@ impl IsoSectorReader {
if sectors > u32::MAX as u64 {
return Err(crate::error::Error::IsoTooLarge {
path: path.to_string(),
}.into());
}
.into());
}
let capacity = sectors as u32;
Ok(Self { file, capacity })
+51 -20
View File
@@ -7,7 +7,11 @@ use super::{meta, ts};
use crate::disc::{DiscTitle, Stream as DiscStream};
use std::io::{self, Read, Write};
type PesSetup = (Vec<u16>, Vec<(u16, Box<dyn super::codec::CodecParser>)>, Vec<(u16, usize)>);
type PesSetup = (
Vec<u16>,
Vec<(u16, Box<dyn super::codec::CodecParser>)>,
Vec<(u16, usize)>,
);
/// Size of initial scan buffer for PMT/stream detection.
const SCAN_SIZE: usize = 1024 * 1024;
@@ -59,11 +63,15 @@ impl M2tsStream {
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 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() {
@@ -117,11 +125,16 @@ impl M2tsStream {
let (pids, parsers, pid_to_track) = Self::setup_pes(&title.streams);
// Chain: remaining head bytes + rest of reader
let remaining_head = &head[header_end..];
let chain: Box<dyn Read> = Box::new(io::Cursor::new(remaining_head.to_vec()).chain(reader));
let chain: Box<dyn Read> =
Box::new(io::Cursor::new(remaining_head.to_vec()).chain(reader));
return Ok(Self {
disc_title: title.clone(),
mode: Mode::Read { reader: chain },
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
demuxer: if pids.is_empty() {
None
} else {
Some(ts::TsDemuxer::new(&pids))
},
parsers,
pending_frames: std::collections::VecDeque::new(),
pid_to_track,
@@ -146,7 +159,11 @@ impl M2tsStream {
..DiscTitle::empty()
},
mode: Mode::Read { reader: chain },
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
demuxer: if pids.is_empty() {
None
} else {
Some(ts::TsDemuxer::new(&pids))
},
parsers,
pending_frames: std::collections::VecDeque::new(),
pid_to_track,
@@ -161,7 +178,9 @@ impl crate::pes::Stream for M2tsStream {
if let Some(frame) = self.pending_frames.pop_front() {
return Ok(Some(frame));
}
if self.pes_eof { return Ok(None); }
if self.pes_eof {
return Ok(None);
}
loop {
let reader = match &mut self.mode {
@@ -175,11 +194,15 @@ impl crate::pes::Stream for M2tsStream {
// 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) {
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)
crate::pes::PesFrame::from_codec_frame(*track, frame),
);
}
}
@@ -192,11 +215,15 @@ impl crate::pes::Stream for M2tsStream {
if let Some(ref mut demuxer) = self.demuxer {
let packets = demuxer.feed(&buf[..n]);
for pes in &packets {
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) {
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)
crate::pes::PesFrame::from_codec_frame(*track, frame),
);
}
}
@@ -224,7 +251,9 @@ impl crate::pes::Stream for M2tsStream {
}
}
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
fn info(&self) -> &crate::disc::DiscTitle {
&self.disc_title
}
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
// First check stored codec_privates from FMKV header
@@ -232,10 +261,13 @@ impl crate::pes::Stream for M2tsStream {
return Some(cp.clone());
}
// Fall back to parser-extracted codec_private
let pid = self.pid_to_track.iter()
let pid = self
.pid_to_track
.iter()
.find(|(_, idx)| *idx == track)
.map(|(pid, _)| *pid)?;
self.parsers.iter()
self.parsers
.iter()
.find(|(p, _)| *p == pid)
.and_then(|(_, parser)| parser.codec_private())
}
@@ -251,4 +283,3 @@ impl crate::pes::Stream for M2tsStream {
true
}
}
+31 -14
View File
@@ -93,7 +93,9 @@ impl M2tsMeta {
hdr: v.hdr.id().into(),
label: v.label.clone(),
secondary: v.secondary,
codec_private: title.codec_privates.get(i)
codec_private: title
.codec_privates
.get(i)
.and_then(|cp| cp.as_ref())
.map(|cp| base64::engine::general_purpose::STANDARD.encode(cp)),
},
@@ -141,8 +143,12 @@ impl M2tsMeta {
} => Stream::Video(VideoStream {
pid: *pid,
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
resolution: resolution.parse().unwrap_or(crate::disc::Resolution::Unknown),
frame_rate: frame_rate.parse().unwrap_or(crate::disc::FrameRate::Unknown),
resolution: resolution
.parse()
.unwrap_or(crate::disc::Resolution::Unknown),
frame_rate: frame_rate
.parse()
.unwrap_or(crate::disc::FrameRate::Unknown),
hdr: hdr.parse().unwrap_or(crate::disc::HdrFormat::Sdr),
color_space: ColorSpace::Bt709,
secondary: *secondary,
@@ -159,9 +165,13 @@ impl M2tsMeta {
} => Stream::Audio(AudioStream {
pid: *pid,
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
channels: channels.parse().unwrap_or(crate::disc::AudioChannels::Unknown),
channels: channels
.parse()
.unwrap_or(crate::disc::AudioChannels::Unknown),
language: language.clone(),
sample_rate: sample_rate.parse().unwrap_or(crate::disc::SampleRate::Unknown),
sample_rate: sample_rate
.parse()
.unwrap_or(crate::disc::SampleRate::Unknown),
secondary: *secondary,
label: label.clone(),
}),
@@ -197,16 +207,23 @@ impl M2tsMeta {
/// Extract codec_private data per stream (from FMKV header).
/// Returns a Vec matching stream order — None for streams without codec_private.
pub fn codec_privates(&self) -> Vec<Option<Vec<u8>>> {
self.streams.iter().map(|s| {
if let MetaStream::Video { codec_private: Some(ref b64), .. } = s {
{
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(b64).ok()
self.streams
.iter()
.map(|s| {
if let MetaStream::Video {
codec_private: Some(ref b64),
..
} = s
{
{
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(b64).ok()
}
} else {
None
}
} else {
None
}
}).collect()
})
.collect()
}
}
+39 -17
View File
@@ -40,10 +40,7 @@ pub struct MkvStream {
impl MkvStream {
/// 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> {
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 {
@@ -106,10 +103,14 @@ impl crate::pes::Stream for MkvStream {
}
ebml::SIMPLE_BLOCK => {
let block = ebml::read_binary_val(&mut rs.reader, size as usize)?;
if block.len() < 4 { continue; }
if block.len() < 4 {
continue;
}
let (track, vl) = block_vint(&block);
if vl + 3 > block.len() { continue; }
if vl + 3 > block.len() {
continue;
}
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
let keyframe = block[vl + 2] & 0x80 != 0;
@@ -139,7 +140,9 @@ impl crate::pes::Stream for MkvStream {
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: 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()),
}
@@ -154,7 +157,9 @@ impl crate::pes::Stream for MkvStream {
Ok(())
}
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
fn info(&self) -> &crate::disc::DiscTitle {
&self.disc_title
}
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
let track_num = (track + 1) as u16; // MKV tracks are 1-based
@@ -176,9 +181,7 @@ impl crate::pes::Stream for MkvStream {
// ── MKV header parsing (read side) ────────────────────────────
/// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
fn parse_mkv_header(
r: &mut impl Read,
) -> MkvHeaderResult {
fn parse_mkv_header(r: &mut impl Read) -> MkvHeaderResult {
let mut title = String::new();
let mut duration_ms = 0.0f64;
let mut ts_scale: u64 = 1_000_000;
@@ -220,7 +223,9 @@ fn parse_mkv_header(
ebml::TIMESTAMP_SCALE => ts_scale = ebml::read_uint_val(r, cs as usize)?,
ebml::DURATION => duration_ms = ebml::read_float_val(r, cs as usize)?,
ebml::TITLE => title = ebml::read_string_val(r, cs as usize)?,
_ => { skip_bytes(r, cs)?; }
_ => {
skip_bytes(r, cs)?;
}
}
}
got_info = true;
@@ -303,11 +308,15 @@ fn parse_track(
match aid {
ebml::SAMPLING_FREQUENCY => sr = ebml::read_float_val(r, as_ as usize)?,
ebml::CHANNELS => ch = ebml::read_uint_val(r, as_ as usize)? as u8,
_ => { skip_bytes(r, as_)?; }
_ => {
skip_bytes(r, as_)?;
}
}
}
}
_ => { skip_bytes(r, cs)?; }
_ => {
skip_bytes(r, cs)?;
}
}
}
@@ -334,7 +343,11 @@ fn parse_track(
};
// Map MKV track numbers to BD-TS PIDs
let ts_pid = if tnum == 1 { 0x1011 } else { 0x1100 + (tnum - 2) };
let ts_pid = if tnum == 1 {
0x1011
} else {
0x1100 + (tnum - 2)
};
let stream = match ttype {
1 => {
@@ -382,10 +395,19 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
return ((((d[0] & 0x3F) as u64) << 8) | d[1] as u64, 2);
}
if d[0] & 0x20 != 0 && d.len() >= 3 {
return ((((d[0] & 0x1F) as u64) << 16) | ((d[1] as u64) << 8) | d[2] as u64, 3);
return (
(((d[0] & 0x1F) as u64) << 16) | ((d[1] as u64) << 8) | d[2] as u64,
3,
);
}
if d[0] & 0x10 != 0 && d.len() >= 4 {
return ((((d[0] & 0x0F) as u64) << 24) | ((d[1] as u64) << 16) | ((d[2] as u64) << 8) | d[3] as u64, 4);
return (
(((d[0] & 0x0F) as u64) << 24)
| ((d[1] as u64) << 16)
| ((d[2] as u64) << 8)
| d[3] as u64,
4,
);
}
(0, 1) // Unsupported 5+ byte VINT — treat as track 0
}
+3 -3
View File
@@ -19,11 +19,9 @@ pub mod codec;
pub mod disc;
pub mod ebml;
pub mod iso;
pub mod mkv;
pub mod tsmux;
pub mod tsreader;
mod m2ts;
pub mod meta;
pub mod mkv;
mod mkvstream;
pub mod network;
pub mod null;
@@ -31,6 +29,8 @@ pub mod ps;
pub mod resolve;
pub mod stdio;
pub mod ts;
pub mod tsmux;
pub mod tsreader;
pub use disc::DiscStream;
pub use iso::IsoSectorReader;
+14 -3
View File
@@ -79,7 +79,11 @@ impl crate::pes::Stream for NetworkStream {
}
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
match &mut self.mode {
Mode::Write { writer, ref mut header_written, .. } => {
Mode::Write {
writer,
ref mut header_written,
..
} => {
if !*header_written {
if !self.disc_title.streams.is_empty() {
let m = meta::M2tsMeta::from_title(&self.disc_title);
@@ -99,7 +103,9 @@ impl crate::pes::Stream for NetworkStream {
}
Ok(())
}
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
}
// NetworkStream is PES-only — no IOStream/Read/Write byte interface.
@@ -174,7 +180,12 @@ mod tests {
let dt = sample_title();
let mut writer = NetworkStream::connect(&addr).unwrap().meta(&dt);
let frame = pes::PesFrame { track: 0, pts: 90000, keyframe: true, data: vec![0x47; 192] };
let frame = pes::PesFrame {
track: 0,
pts: 90000,
keyframe: true,
data: vec![0x47; 192],
};
pes::Stream::write(&mut writer, &frame).unwrap();
pes::Stream::finish(&mut writer).unwrap();
+12 -4
View File
@@ -17,8 +17,16 @@ impl NullStream {
}
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 }
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
}
}
-1
View File
@@ -163,7 +163,6 @@ impl PsDemuxer {
packets
}
}
/// Check whether a start code byte is a valid PES stream ID that carries payload.
+41 -42
View File
@@ -128,12 +128,14 @@ fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
if path.as_os_str().is_empty() {
return Err(crate::error::Error::StreamUrlMissingPath {
scheme: scheme.to_string(),
}.into());
}
.into());
}
if path.file_name().is_none() {
return Err(crate::error::Error::StreamUrlInvalid {
url: format!("{scheme}://{}", path.display()),
}.into());
}
.into());
}
Ok(())
}
@@ -143,12 +145,14 @@ fn validate_network_addr(addr: &str) -> io::Result<()> {
if addr.is_empty() {
return Err(crate::error::Error::StreamUrlMissingPath {
scheme: "network".to_string(),
}.into());
}
.into());
}
if !addr.contains(':') {
return Err(crate::error::Error::StreamUrlMissingPort {
addr: addr.to_string(),
}.into());
}
.into());
}
Ok(())
}
@@ -169,10 +173,15 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
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() })?,
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();
@@ -181,7 +190,8 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
drive,
opts.keydb_path.as_deref(),
opts.title_index.unwrap_or(0),
).map_err(|e| -> io::Error { e.into() })?;
)
.map_err(|e| -> io::Error { e.into() })?;
if opts.raw {
stream.set_raw();
}
@@ -193,7 +203,8 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
Some(p) => crate::disc::ScanOptions::with_keydb(p),
None => crate::disc::ScanOptions::default(),
};
let mut stream = DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
let mut stream =
DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
if opts.raw {
stream.set_raw();
}
@@ -201,15 +212,17 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
}
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 file = std::fs::File::open(path).map_err(|e| {
io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e))
})?;
let reader = std::io::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 file = std::fs::File::open(path).map_err(|e| {
io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e))
})?;
let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(MkvStream::open(reader)?))
}
@@ -217,16 +230,10 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
validate_network_addr(addr)?;
Ok(Box::new(NetworkStream::listen(addr)?))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::input()))
}
StreamUrl::Null => {
Err(crate::error::Error::StreamWriteOnly.into())
}
StreamUrl::Stdio => Ok(Box::new(StdioStream::input())),
StreamUrl::Null => Err(crate::error::Error::StreamWriteOnly.into()),
StreamUrl::Unknown { ref raw } => {
Err(crate::error::Error::StreamUrlInvalid {
url: raw.clone(),
}.into())
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
}
}
}
@@ -240,16 +247,18 @@ pub fn output(
match parsed {
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 file = std::fs::File::create(path).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(MkvStream::create(writer, title)?))
}
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 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)?))
}
@@ -257,22 +266,12 @@ pub fn output(
validate_network_addr(addr)?;
Ok(Box::new(NetworkStream::connect(addr)?.meta(title)))
}
StreamUrl::Stdio => {
Ok(Box::new(StdioStream::output(title)))
}
StreamUrl::Null => {
Ok(Box::new(NullStream::new(title)))
}
StreamUrl::Disc { .. } => {
Err(crate::error::Error::StreamReadOnly.into())
}
StreamUrl::Iso { .. } => {
Err(crate::error::Error::StreamReadOnly.into())
}
StreamUrl::Stdio => Ok(Box::new(StdioStream::output(title))),
StreamUrl::Null => Ok(Box::new(NullStream::new(title))),
StreamUrl::Disc { .. } => Err(crate::error::Error::StreamReadOnly.into()),
StreamUrl::Iso { .. } => Err(crate::error::Error::StreamReadOnly.into()),
StreamUrl::Unknown { ref raw } => {
Err(crate::error::Error::StreamUrlInvalid {
url: raw.clone(),
}.into())
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
}
}
}
+9 -3
View File
@@ -84,13 +84,19 @@ impl crate::pes::Stream for StdioStream {
}
}
fn finish(&mut self) -> io::Result<()> {
if let Some(w) = &mut self.writer { w.flush()?; }
if let Some(w) = &mut self.writer {
w.flush()?;
}
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())
self.stored_codec_privates
.get(track)
.and_then(|c| c.clone())
}
fn headers_ready(&self) -> bool {
+4 -1
View File
@@ -329,7 +329,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
| data[pat_start + 2] as usize;
let entries_start = pat_start + 8;
if section_len < 4 { offset += BD_TS_PACKET_SIZE; continue; }
if section_len < 4 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let entries_end = pat_start + 3 + section_len - 4;
let mut e = entries_start;
while e + 4 <= data.len() && e < entries_end {
+21 -14
View File
@@ -12,9 +12,9 @@ const TS_PAYLOAD: usize = 184;
pub struct TsMuxer<W: Write> {
writer: W,
pids: Vec<u16>,
continuity: Vec<u8>, // per-PID continuity counter (0-15)
continuity: Vec<u8>, // per-PID continuity counter (0-15)
codec_privates: Vec<Option<Vec<u8>>>, // per-track codec_private (for video parameter sets)
params_written: Vec<bool>, // per-track: have we written parameter sets?
params_written: Vec<bool>, // per-track: have we written parameter sets?
}
impl<W: Write> TsMuxer<W> {
@@ -40,12 +40,7 @@ impl<W: Write> TsMuxer<W> {
/// Write a PES frame as BD-TS packets.
/// Video frame data is expected as length-prefixed NALUs (MKV/PES format)
/// and is converted to Annex B for transport stream.
pub fn write_frame(
&mut self,
track: usize,
pts_ns: i64,
data: &[u8],
) -> io::Result<()> {
pub fn write_frame(&mut self, track: usize, pts_ns: i64, data: &[u8]) -> io::Result<()> {
if track >= self.pids.len() {
return Ok(()); // unknown track, skip
}
@@ -125,11 +120,13 @@ impl<W: Write> TsMuxer<W> {
self.writer.write_all(&STUFF_FF[..stuff_len - 2])?;
}
}
self.writer.write_all(&pes_packet[offset..offset + payload_len])?;
self.writer
.write_all(&pes_packet[offset..offset + payload_len])?;
} else {
self.writer.write_all(&tp_extra)?;
self.writer.write_all(&ts_header)?;
self.writer.write_all(&pes_packet[offset..offset + payload_len])?;
self.writer
.write_all(&pes_packet[offset..offset + payload_len])?;
}
offset += payload_len;
@@ -202,24 +199,34 @@ fn hvcc_to_annex_b(hvcc: &[u8]) -> Option<Vec<u8>> {
let mut offset = 23;
for _ in 0..num_arrays {
if offset + 3 > hvcc.len() { break; }
if offset + 3 > hvcc.len() {
break;
}
// array: 1 byte (completeness + NAL type), 2 bytes (numNalus)
let _nal_type = hvcc[offset] & 0x3F;
let num_nalus = u16::from_be_bytes([hvcc[offset + 1], hvcc[offset + 2]]) as usize;
offset += 3;
for _ in 0..num_nalus {
if offset + 2 > hvcc.len() { break; }
if offset + 2 > hvcc.len() {
break;
}
let nal_len = u16::from_be_bytes([hvcc[offset], hvcc[offset + 1]]) as usize;
offset += 2;
if offset + nal_len > hvcc.len() { break; }
if offset + nal_len > hvcc.len() {
break;
}
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
out.extend_from_slice(&hvcc[offset..offset + nal_len]);
offset += nal_len;
}
}
if out.is_empty() { None } else { Some(out) }
if out.is_empty() {
None
} else {
Some(out)
}
}
/// Convert length-prefixed NALUs (4-byte BE length + NAL) to Annex B
+12 -5
View File
@@ -69,10 +69,14 @@ impl<R: Read> TsDemuxReader<R> {
let packets = self.demuxer.feed(&self.buf[..n]);
for pes in &packets {
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) {
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.push_back(PesFrame::from_codec_frame(*track, frame));
self.pending
.push_back(PesFrame::from_codec_frame(*track, frame));
}
}
}
@@ -86,10 +90,13 @@ impl<R: Read> TsDemuxReader<R> {
/// Codec private data for a track.
pub fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
let pid = self.pid_to_track.iter()
let pid = self
.pid_to_track
.iter()
.find(|(_, idx)| *idx == track)
.map(|(pid, _)| *pid)?;
self.parsers.iter()
self.parsers
.iter()
.find(|(p, _)| *p == pid)
.and_then(|(_, parser)| parser.codec_private())
}
+15 -6
View File
@@ -28,7 +28,8 @@ impl PesFrame {
if self.data.len() > u32::MAX as usize {
return Err(crate::error::Error::PesFrameTooLarge {
size: self.data.len(),
}.into());
}
.into());
}
w.write_all(&[self.track as u8])?;
w.write_all(&self.pts.to_le_bytes())?;
@@ -49,8 +50,7 @@ impl PesFrame {
}
let track = header[0] as usize;
let pts = i64::from_le_bytes([
header[1], header[2], header[3], header[4],
header[5], header[6], header[7], header[8],
header[1], header[2], header[3], header[4], header[5], header[6], header[7], header[8],
]);
let keyframe = header[9] != 0;
let len = u32::from_le_bytes([header[10], header[11], header[12], header[13]]) as usize;
@@ -59,7 +59,12 @@ impl PesFrame {
}
let mut data = vec![0u8; len];
r.read_exact(&mut data)?;
Ok(Some(Self { track, pts, keyframe, data }))
Ok(Some(Self {
track,
pts,
keyframe,
data,
}))
}
/// Create from a codec::Frame with a track index.
@@ -88,10 +93,14 @@ pub trait Stream {
fn info(&self) -> &crate::disc::DiscTitle;
/// Codec initialization data for a track (SPS/PPS, etc).
fn codec_private(&self, _track: usize) -> Option<Vec<u8>> { None }
fn codec_private(&self, _track: usize) -> Option<Vec<u8>> {
None
}
/// True when codec_private is available for all video tracks.
fn headers_ready(&self) -> bool { true }
fn headers_ready(&self) -> bool {
true
}
}
/// Wraps any output stream and counts bytes written.
+5 -5
View File
@@ -166,7 +166,10 @@ impl SgIoTransport {
let err = std::io::Error::last_os_error();
Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
Error::DevicePermission {
path: format!("{}: permission denied (try running as root)", device.display()),
path: format!(
"{}: permission denied (try running as root)",
device.display()
),
}
} else {
Error::DeviceNotFound {
@@ -225,10 +228,7 @@ impl SgIoTransport {
if let Ok(mut entries) = std::fs::read_dir(&sg_dir) {
if let Some(Ok(entry)) = entries.next() {
let sg_name = entry.file_name();
return std::path::PathBuf::from(format!(
"/dev/{}",
sg_name.to_string_lossy()
));
return std::path::PathBuf::from(format!("/dev/{}", sg_name.to_string_lossy()));
}
}
}
+7 -3
View File
@@ -13,7 +13,9 @@ pub trait SectorReader: Send {
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
/// Total capacity in sectors, if known.
fn capacity(&self) -> u32 { 0 }
fn capacity(&self) -> u32 {
0
}
}
/// SectorReader backed by a file (ISO image).
@@ -47,9 +49,11 @@ impl SectorReader for FileSectorReader {
use std::io::{Read, Seek, SeekFrom};
let offset = lba as u64 * 2048;
let bytes = count as usize * 2048;
self.file.seek(SeekFrom::Start(offset))
self.file
.seek(SeekFrom::Start(offset))
.map_err(|e| crate::error::Error::IoError { source: e })?;
self.file.read_exact(&mut buf[..bytes])
self.file
.read_exact(&mut buf[..bytes])
.map_err(|e| crate::error::Error::IoError { source: e })?;
Ok(bytes)
}
+5 -1
View File
@@ -416,7 +416,11 @@ fn batch_count_u16_overflow_regression() {
// 47513600 = 725 * 65536, lower 16 bits = 0
let remaining: u32 = 47533152 - 19552; // = 47513600
assert_eq!(remaining, 47513600);
assert_eq!(remaining % 65536, 0, "remaining should be multiple of 65536");
assert_eq!(
remaining % 65536,
0,
"remaining should be multiple of 65536"
);
// Buggy version produces 0 → infinite loop
assert_eq!(buggy_batch_count(remaining, 60), 0);
+30 -6
View File
@@ -121,7 +121,11 @@ fn open_input_bare_path_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("not a valid stream URL") || msg.contains("E9002"), "got: {}", msg);
assert!(
msg.contains("not a valid stream URL") || msg.contains("E9002"),
"got: {}",
msg
);
}
#[test]
@@ -133,7 +137,11 @@ fn open_output_bare_path_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("not a valid stream URL") || msg.contains("E9002"), "got: {}", msg);
assert!(
msg.contains("not a valid stream URL") || msg.contains("E9002"),
"got: {}",
msg
);
}
#[test]
@@ -144,7 +152,11 @@ fn open_input_m2ts_empty_path_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("requires a file path") || msg.contains("E9003"), "got: {}", msg);
assert!(
msg.contains("requires a file path") || msg.contains("E9003"),
"got: {}",
msg
);
}
#[test]
@@ -155,7 +167,11 @@ fn open_output_null_input_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("write-only") || msg.contains("E9001"), "got: {}", msg);
assert!(
msg.contains("write-only") || msg.contains("E9001"),
"got: {}",
msg
);
}
#[test]
@@ -167,7 +183,11 @@ fn open_output_disc_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("read-only") || msg.contains("E9000"), "got: {}", msg);
assert!(
msg.contains("read-only") || msg.contains("E9000"),
"got: {}",
msg
);
}
#[test]
@@ -178,7 +198,11 @@ fn open_input_network_no_port_errors() {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(msg.contains("PES pipeline") || msg.contains("missing port") || msg.contains("E9004"), "got: {}", msg);
assert!(
msg.contains("PES pipeline") || msg.contains("missing port") || msg.contains("E9004"),
"got: {}",
msg
);
}
#[test]