Unified Stream trait: read() and write() on one type

Stream trait: read() returns PesFrame, write() accepts PesFrame.
A stream is a stream — you read from it or write to it.
No separate Input/Output traits.

API: libfreemkv::input(url) and libfreemkv::output(url, title, codecs)
Returns Box<dyn Stream>.
This commit is contained in:
MattJackson
2026-04-15 03:33:29 +00:00
parent bd644d2f60
commit ff6004a567
33 changed files with 689 additions and 400 deletions
+1 -4
View File
@@ -1050,10 +1050,7 @@ pub fn read_volume_id(session: &mut Drive, auth: &mut AacsAuth) -> Result<[u8; 1
}
/// Read data keys after successful authentication (for AACS 2.0 bus encryption).
pub fn read_data_keys(
session: &mut Drive,
auth: &mut AacsAuth,
) -> Result<([u8; 16], [u8; 16])> {
pub fn read_data_keys(session: &mut Drive, auth: &mut AacsAuth) -> Result<([u8; 16], [u8; 16])> {
// REPORT DISC STRUCTURE format 0x84
let cdb = cdb_report_disc_structure(auth.agid, 0x84, 36);
let response = scsi_read(session, &cdb, 36).map_err(|_| Error::AacsDataKey)?;
+7 -5
View File
@@ -188,9 +188,13 @@ pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Opt
// Try each processing key against each UV/cvalue pair
for pk in processing_keys {
for i in 0..num_uvs {
if (i + 1) * 16 > cvalues.len() { continue; }
if (i + 1) * 16 > cvalues.len() {
continue;
}
let record_start = i * 5;
if record_start + 5 > uvs.len() { continue; }
if record_start + 5 > uvs.len() {
continue;
}
let _u_mask_shift = uvs[record_start];
let uv = &uvs[record_start + 1..record_start + 5];
let cv = &cvalues[i * 16..(i + 1) * 16];
@@ -448,9 +452,7 @@ const MKB_PACK_SIZE: usize = 32772;
/// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83).
/// Returns the concatenated MKB data from all packs.
pub fn read_mkb_from_drive(
session: &mut crate::drive::Drive,
) -> crate::error::Result<Vec<u8>> {
pub fn read_mkb_from_drive(session: &mut crate::drive::Drive) -> crate::error::Result<Vec<u8>> {
use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE};
let cdb = [
+4 -4
View File
@@ -104,8 +104,8 @@ impl Disc {
1 | 6 | 7 => Some(Stream::Video(VideoStream {
pid: s.pid,
codec,
resolution: format_resolution(s.video_format, s.video_rate),
frame_rate: format_framerate(s.video_rate),
resolution: Resolution::from_video_format(s.video_format),
frame_rate: FrameRate::from_video_rate(s.video_rate),
hdr: match s.dynamic_range {
1 => HdrFormat::Hdr10,
2 => HdrFormat::DolbyVision,
@@ -137,9 +137,9 @@ impl Disc {
Some(Stream::Audio(AudioStream {
pid: s.pid,
codec,
channels: format_channels(s.audio_format),
channels: AudioChannels::from_audio_format(s.audio_format),
language: s.language.clone(),
sample_rate: format_samplerate(s.audio_rate),
sample_rate: SampleRate::from_audio_rate(s.audio_rate),
secondary: s.stream_type == 5,
label: String::new(),
}))
+7 -32
View File
@@ -20,20 +20,13 @@ impl Disc {
let mut title_number: u16 = 0;
for ts in &dvd_info.title_sets {
// Map DvdVideoAttr to Stream::Video
let video_codec = match ts.video.codec.as_str() {
"mpeg2" => Codec::Mpeg2,
"mpeg1" => Codec::Mpeg2, // treat MPEG-1 as MPEG-2 for container purposes
_ => Codec::Mpeg2,
};
let video_stream = Stream::Video(VideoStream {
pid: 0xE0, // DVD video PID (standard MPEG PS video stream)
codec: video_codec,
resolution: ts.video.resolution.clone(),
codec: ts.video.codec,
resolution: ts.video.resolution,
frame_rate: match ts.video.standard.as_str() {
"PAL" => "25".to_string(),
_ => "29.97".to_string(),
"PAL" => FrameRate::F25,
_ => FrameRate::F29_97,
},
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
@@ -47,31 +40,13 @@ impl Disc {
.iter()
.enumerate()
.map(|(i, a)| {
let codec = match a.codec.as_str() {
"ac3" => Codec::Ac3,
"dts" => Codec::Dts,
"lpcm" => Codec::Lpcm,
"mpeg1" | "mpeg2" => Codec::Mpeg2,
_ => Codec::Unknown(0),
};
let channels = match a.channels {
1 => "mono".to_string(),
2 => "stereo".to_string(),
6 => "5.1".to_string(),
8 => "7.1".to_string(),
n => format!("{n}ch"),
};
let sample_rate = match a.sample_rate {
48000 => "48kHz".to_string(),
96000 => "96kHz".to_string(),
sr => format!("{}kHz", sr / 1000),
};
let codec = a.codec;
Stream::Audio(AudioStream {
pid: 0xBD00 + i as u16, // DVD private stream 1 sub-IDs
codec,
channels,
channels: AudioChannels::from_count(a.channels),
language: a.language.clone(),
sample_rate,
sample_rate: SampleRate::from_hz(a.sample_rate),
secondary: false,
label: String::new(),
})
+24 -12
View File
@@ -67,8 +67,8 @@ pub fn capture_drive_data(session: &mut Drive) -> Result<DriveCapture> {
}
// Vendor-specific READ_BUFFER queries
let rb_f1 = session.read_buffer(0x02, 0xF1, 48); // Pioneer
let rb_mode6 = session.read_buffer(0x06, 0x00, 32); // MTK
let rb_f1 = session.read_buffer(0x02, 0xF1, 48); // Pioneer
let rb_mode6 = session.read_buffer(0x06, 0x00, 32); // MTK
// Standard queries
let rpc_state = session.report_key_rpc_state();
@@ -87,18 +87,30 @@ pub fn capture_drive_data(session: &mut Drive) -> Result<DriveCapture> {
/// Mask a string for privacy (letters->A, digits->0).
pub fn mask_string(s: &str) -> String {
s.chars().map(|c| {
if c.is_ascii_alphabetic() { 'A' }
else if c.is_ascii_digit() { '0' }
else { c }
}).collect()
s.chars()
.map(|c| {
if c.is_ascii_alphabetic() {
'A'
} else if c.is_ascii_digit() {
'0'
} else {
c
}
})
.collect()
}
/// Mask bytes for privacy.
pub fn mask_bytes(data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| {
if b.is_ascii_alphabetic() { b'A' }
else if b.is_ascii_digit() { b'0' }
else { b }
}).collect()
data.iter()
.map(|&b| {
if b.is_ascii_alphabetic() {
b'A'
} else if b.is_ascii_digit() {
b'0'
} else {
b
}
})
.collect()
}
+8 -3
View File
@@ -10,6 +10,11 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
if !std::path::Path::new(&path).exists() {
continue;
}
// Skip stale device nodes — sysfs entry must exist
let sysfs = format!("/sys/class/scsi_generic/sg{i}/device/model");
if !std::path::Path::new(&sysfs).exists() {
continue;
}
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
if !id.raw_inquiry.is_empty() && (id.raw_inquiry[0] & 0x1F) == 0x05 {
@@ -21,6 +26,7 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
drives
}
#[allow(dead_code)]
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
if path.contains("/sg") {
if !std::path::Path::new(path).exists() {
@@ -39,9 +45,8 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
&& sg_id.product_id == sr_id.product_id
&& sg_id.serial_number == sr_id.serial_number
{
let warning = format!(
"{path} is a block device (sr) — using {sg_path} (sg) for raw access"
);
let warning =
format!("{path} is a block device (sr) — using {sg_path} (sg) for raw access");
return Ok((sg_path, Some(warning)));
}
}
+3 -1
View File
@@ -7,7 +7,9 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
let mut drives = Vec::new();
for i in 0..16 {
let path = format!("/dev/disk{}", i);
if !std::path::Path::new(&path).exists() { continue; }
if !std::path::Path::new(&path).exists() {
continue;
}
match crate::scsi::open(std::path::Path::new(&path)) {
Ok(mut transport) => {
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
+19
View File
@@ -25,6 +25,25 @@ pub struct Event {
/// Types of events the lib can fire.
#[derive(Debug)]
pub enum EventKind {
// ── Init sequence events ────────────────────────────────────────
/// Drive opened successfully.
DriveOpened { device: String },
/// Drive is ready (disc spun up).
DriveReady,
/// Firmware init completed.
InitComplete { success: bool },
/// Disc probe completed.
ProbeComplete { success: bool },
/// Disc scan completed.
ScanComplete { titles: usize },
// ── Read events ─────────────────────────────────────────────────
/// Bytes successfully read and written to output.
BytesRead {
/// Bytes written so far.
+26 -51
View File
@@ -7,6 +7,7 @@
//! The parser reads IFO files via UDF and extracts enough information
//! to build DiscTitle structs (parallel to MPLS for Blu-ray).
use crate::disc::{Codec, Resolution};
use crate::error::{Error, Result};
use crate::sector::SectorReader;
use crate::udf::UdfFs;
@@ -61,8 +62,8 @@ pub struct DvdCell {
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DvdVideoAttr {
pub codec: String,
pub resolution: String,
pub codec: Codec,
pub resolution: Resolution,
pub aspect: String,
pub standard: String,
}
@@ -70,7 +71,7 @@ pub struct DvdVideoAttr {
/// DVD audio stream attributes.
#[derive(Debug, Clone)]
pub struct DvdAudioAttr {
pub codec: String,
pub codec: Codec,
pub channels: u8,
pub sample_rate: u32,
pub language: String,
@@ -349,41 +350,15 @@ fn parse_video_attr(data: &[u8]) -> Result<DvdVideoAttr> {
_ => "4:3",
};
let resolution = match (b0 >> 4) & 0x03 {
0 => {
if standard == "PAL" {
"720x576"
} else {
"720x480"
}
}
1 => {
if standard == "PAL" {
"704x576"
} else {
"704x480"
}
}
2 => {
if standard == "PAL" {
"352x576"
} else {
"352x480"
}
}
3 => {
if standard == "PAL" {
"352x288"
} else {
"352x240"
}
}
_ => "720x480",
let resolution = if standard == "PAL" {
Resolution::R576i
} else {
Resolution::R480i
};
Ok(DvdVideoAttr {
codec: "mpeg2".to_string(),
resolution: resolution.to_string(),
codec: Codec::Mpeg2,
resolution,
aspect: aspect.to_string(),
standard: standard.to_string(),
})
@@ -396,12 +371,12 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
let coding_mode = (b0 >> 5) & 0x07;
let codec = match coding_mode {
0 => "ac3",
2 => "mpeg1",
3 => "mpeg2",
4 => "lpcm",
6 => "dts",
_ => "unknown",
0 => Codec::Ac3,
2 => Codec::Mpeg1,
3 => Codec::Mp2,
4 => Codec::Lpcm,
6 => Codec::Dts,
_ => Codec::Unknown(coding_mode),
};
let sample_rate_flag = (b0 >> 3) & 0x03;
@@ -434,7 +409,7 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
};
Ok(DvdAudioAttr {
codec: codec.to_string(),
codec,
channels,
sample_rate,
language,
@@ -688,15 +663,15 @@ mod tests {
assert_eq!(title.cells.len(), 1);
let video = DvdVideoAttr {
codec: "mpeg2".to_string(),
resolution: "720x480".to_string(),
codec: Codec::Mpeg2,
resolution: Resolution::R480i,
aspect: "16:9".to_string(),
standard: "NTSC".to_string(),
};
assert_eq!(video.codec, "mpeg2");
assert_eq!(video.codec, Codec::Mpeg2);
let audio = DvdAudioAttr {
codec: "ac3".to_string(),
codec: Codec::Ac3,
channels: 6,
sample_rate: 48000,
language: "en".to_string(),
@@ -730,8 +705,8 @@ mod tests {
let attr = parse_video_attr(&data).unwrap();
assert_eq!(attr.standard, "NTSC");
assert_eq!(attr.aspect, "16:9");
assert_eq!(attr.resolution, "720x480");
assert_eq!(attr.codec, "mpeg2");
assert_eq!(attr.resolution, Resolution::R480i);
assert_eq!(attr.codec, Codec::Mpeg2);
}
#[test]
@@ -743,7 +718,7 @@ mod tests {
let attr = parse_video_attr(&data).unwrap();
assert_eq!(attr.standard, "PAL");
assert_eq!(attr.aspect, "4:3");
assert_eq!(attr.resolution, "720x576");
assert_eq!(attr.resolution, Resolution::R576i);
}
#[test]
@@ -759,7 +734,7 @@ mod tests {
data[3] = b'n';
let attr = parse_audio_attr(&data, 0).unwrap();
assert_eq!(attr.codec, "ac3");
assert_eq!(attr.codec, Codec::Ac3);
assert_eq!(attr.sample_rate, 48000);
assert_eq!(attr.channels, 6);
assert_eq!(attr.language, "en");
@@ -777,7 +752,7 @@ mod tests {
data[3] = b'r';
let attr = parse_audio_attr(&data, 0).unwrap();
assert_eq!(attr.codec, "dts");
assert_eq!(attr.codec, Codec::Dts);
assert_eq!(attr.sample_rate, 96000);
assert_eq!(attr.channels, 2);
assert_eq!(attr.language, "fr");
+1 -1
View File
@@ -110,7 +110,7 @@ pub use mux::MkvStream;
pub use mux::NetworkStream;
pub use mux::NullStream;
pub use mux::StdioStream;
pub use mux::{open_input, open_output, open_pes_input, open_pes_output, parse_url, InputOptions, StreamUrl};
pub use mux::{input, output, open_input, open_output, parse_url, InputOptions, StreamUrl};
pub use scsi::ScsiTransport;
pub use sector::SectorReader;
pub use speed::DriveSpeed;
+6 -6
View File
@@ -111,12 +111,12 @@ impl CodecParser for H264Parser {
// pictureParameterSetNALUnit = pps
let mut record = vec![
1, // configurationVersion
sps[1], // profile
sps[2], // compatibility
sps[3], // level
0xFF, // 6 bits reserved (111111) + 2 bits lengthSizeMinusOne (11 = 3)
0xE1, // 3 bits reserved (111) + 5 bits numSPS (1)
1, // configurationVersion
sps[1], // profile
sps[2], // compatibility
sps[3], // level
0xFF, // 6 bits reserved (111111) + 2 bits lengthSizeMinusOne (11 = 3)
0xE1, // 3 bits reserved (111) + 5 bits numSPS (1)
(sps.len() >> 8) as u8,
sps.len() as u8,
];
+2 -1
View File
@@ -172,7 +172,8 @@ fn parse_vc1_resolution(sh: &[u8]) -> Option<(u32, u32)> {
}
fn find_next_sc(data: &[u8], from: usize) -> Option<usize> {
(from..data.len().saturating_sub(2)).find(|&i| data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01)
(from..data.len().saturating_sub(2))
.find(|&i| data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01)
}
#[cfg(test)]
+11 -2
View File
@@ -333,8 +333,8 @@ impl IOStream for DiscStream {
}
}
impl crate::pes::InputStream for DiscStream {
fn next_frame(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
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));
@@ -399,6 +399,15 @@ impl crate::pes::InputStream for DiscStream {
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "disc is read-only"))
}
fn finish(&mut self) -> io::Result<()> {
self.drive.unlock_tray();
Ok(())
}
fn info(&self) -> &DiscTitle {
&self.title
}
+8 -2
View File
@@ -256,8 +256,8 @@ impl IsoStream {
}
}
impl crate::pes::InputStream for IsoStream {
fn next_frame(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
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));
@@ -301,6 +301,12 @@ impl crate::pes::InputStream for IsoStream {
}
}
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
}
+1 -1
View File
@@ -412,7 +412,7 @@ impl<W: Write + Seek> IsoWriter<W> {
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 + SECTOR_SIZE - 1) / SECTOR_SIZE) as u32;
let extent_sectors = extent_len.div_ceil(SECTOR_SIZE) as u32;
sector_pos += extent_sectors;
remaining -= extent_len;
}
+19 -74
View File
@@ -3,9 +3,7 @@
//! Format: [8B magic] [4B json_len] [JSON] [padding to 192B boundary] [BD-TS data...]
//! Other tools skip the header during TS sync recovery (scan for 0x47).
use crate::disc::{
AudioStream, Codec, ColorSpace, DiscTitle, HdrFormat, Stream, SubtitleStream, VideoStream,
};
use crate::disc::{AudioStream, ColorSpace, DiscTitle, Stream, SubtitleStream, VideoStream};
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Seek, SeekFrom, Write};
@@ -84,25 +82,25 @@ impl M2tsMeta {
.map(|s| match s {
Stream::Video(v) => MetaStream::Video {
pid: v.pid,
codec: codec_to_str(v.codec),
resolution: v.resolution.clone(),
frame_rate: v.frame_rate.clone(),
hdr: hdr_to_str(v.hdr),
codec: v.codec.id().into(),
resolution: v.resolution.to_string(),
frame_rate: v.frame_rate.to_string(),
hdr: v.hdr.id().into(),
label: v.label.clone(),
secondary: v.secondary,
},
Stream::Audio(a) => MetaStream::Audio {
pid: a.pid,
codec: codec_to_str(a.codec),
channels: a.channels.clone(),
codec: a.codec.id().into(),
channels: a.channels.to_string(),
language: a.language.clone(),
sample_rate: a.sample_rate.clone(),
sample_rate: a.sample_rate.to_string(),
label: a.label.clone(),
secondary: a.secondary,
},
Stream::Subtitle(s) => MetaStream::Subtitle {
pid: s.pid,
codec: codec_to_str(s.codec),
codec: s.codec.id().into(),
language: s.language.clone(),
forced: s.forced,
},
@@ -133,10 +131,10 @@ impl M2tsMeta {
secondary,
} => Stream::Video(VideoStream {
pid: *pid,
codec: str_to_codec(codec),
resolution: resolution.clone(),
frame_rate: frame_rate.clone(),
hdr: str_to_hdr(hdr),
codec: codec.parse().unwrap(),
resolution: resolution.parse().unwrap(),
frame_rate: frame_rate.parse().unwrap(),
hdr: hdr.parse().unwrap(),
color_space: ColorSpace::Bt709,
secondary: *secondary,
label: label.clone(),
@@ -151,10 +149,10 @@ impl M2tsMeta {
secondary,
} => Stream::Audio(AudioStream {
pid: *pid,
codec: str_to_codec(codec),
channels: channels.clone(),
codec: codec.parse().unwrap(),
channels: channels.parse().unwrap(),
language: language.clone(),
sample_rate: sample_rate.clone(),
sample_rate: sample_rate.parse().unwrap(),
secondary: *secondary,
label: label.clone(),
}),
@@ -165,7 +163,7 @@ impl M2tsMeta {
forced,
} => Stream::Subtitle(SubtitleStream {
pid: *pid,
codec: str_to_codec(codec),
codec: codec.parse().unwrap(),
language: language.clone(),
forced: *forced,
codec_data: None,
@@ -277,58 +275,5 @@ pub fn read_header_from_stream(r: &mut impl Read) -> io::Result<Option<M2tsMeta>
Ok(Some(meta))
}
// Codec string conversion (compact, no English — just codec identifiers)
fn codec_to_str(c: Codec) -> String {
match c {
Codec::Hevc => "hevc",
Codec::H264 => "h264",
Codec::Vc1 => "vc1",
Codec::Mpeg2 => "mpeg2",
Codec::TrueHd => "truehd",
Codec::DtsHdMa => "dtshd_ma",
Codec::DtsHdHr => "dtshd_hr",
Codec::Dts => "dts",
Codec::Ac3 => "ac3",
Codec::Ac3Plus => "eac3",
Codec::Lpcm => "lpcm",
Codec::Pgs => "pgs",
Codec::DvdSub => "dvdsub",
Codec::Unknown(_) => "unknown",
}
.into()
}
fn str_to_codec(s: &str) -> Codec {
match s {
"hevc" => Codec::Hevc,
"h264" => Codec::H264,
"vc1" => Codec::Vc1,
"mpeg2" => Codec::Mpeg2,
"truehd" => Codec::TrueHd,
"dtshd_ma" => Codec::DtsHdMa,
"dtshd_hr" => Codec::DtsHdHr,
"dts" => Codec::Dts,
"ac3" => Codec::Ac3,
"eac3" => Codec::Ac3Plus,
"lpcm" => Codec::Lpcm,
"pgs" => Codec::Pgs,
_ => Codec::Unknown(0),
}
}
fn hdr_to_str(h: HdrFormat) -> String {
match h {
HdrFormat::Sdr => "sdr",
HdrFormat::Hdr10 => "hdr10",
HdrFormat::DolbyVision => "dv",
}
.into()
}
fn str_to_hdr(s: &str) -> HdrFormat {
match s {
"hdr10" => HdrFormat::Hdr10,
"dv" => HdrFormat::DolbyVision,
_ => HdrFormat::Sdr,
}
}
// Serialization uses Codec::id() / HdrFormat::id() and Display impls.
// Deserialization uses FromStr impls (.parse()) on each enum.
+5 -42
View File
@@ -35,7 +35,7 @@ impl MkvTrack {
Codec::Mpeg2 => "V_MPEG2",
_ => "V_MPEG2",
};
let (w, h) = parse_resolution(&v.resolution);
let (w, h) = v.resolution.pixels();
Self {
track_type: ebml::TRACK_TYPE_VIDEO,
codec_id,
@@ -61,8 +61,8 @@ impl MkvTrack {
Codec::Lpcm => "A_PCM/INT/BIG",
_ => "A_AC3",
};
let sr = parse_sample_rate(&a.sample_rate);
let ch = parse_channels(&a.channels);
let sr = a.sample_rate.hz();
let ch = a.channels.count();
Self {
track_type: ebml::TRACK_TYPE_AUDIO,
codec_id,
@@ -414,45 +414,8 @@ impl<W: Write + Seek> MkvMuxer<W> {
// Helpers
// ============================================================
fn parse_resolution(s: &str) -> (u32, u32) {
if s.contains("2160") {
(3840, 2160)
} else if s.contains("1080") {
(1920, 1080)
} else if s.contains("720") {
(1280, 720)
} else if s.contains("576") {
(720, 576)
} else if s.contains("480") {
(720, 480)
} else {
(1920, 1080)
}
}
fn parse_sample_rate(s: &str) -> f64 {
if s.contains("192") {
192_000.0
} else if s.contains("96") {
96000.0
} else {
48000.0
}
}
fn parse_channels(s: &str) -> u8 {
if s.contains("7.1") {
8
} else if s.contains("5.1") {
6
} else if s.contains("stereo") || s.contains("2.0") {
2
} else if s.contains("mono") {
1
} else {
6
}
}
// Old parse_resolution/parse_sample_rate/parse_channels removed —
// Resolution::pixels(), SampleRate::hz(), AudioChannels::count() replace them.
#[cfg(test)]
mod tests {
+11 -4
View File
@@ -6,11 +6,12 @@
use super::mkv::{MkvMuxer, MkvTrack};
use super::WriteSeek;
use crate::disc::DiscTitle;
use crate::pes::{OutputStream, PesFrame};
use crate::pes::PesFrame;
use std::io;
pub struct MkvOutputStream {
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
title: DiscTitle,
}
impl MkvOutputStream {
@@ -43,12 +44,16 @@ impl MkvOutputStream {
&title.chapters,
)?;
Ok(Self { muxer: Some(muxer) })
Ok(Self { muxer: Some(muxer), title: title.clone() })
}
}
impl OutputStream for MkvOutputStream {
fn write_frame(&mut self, frame: &PesFrame) -> io::Result<()> {
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 {
@@ -63,4 +68,6 @@ impl OutputStream for MkvOutputStream {
Ok(())
}
}
fn info(&self) -> &DiscTitle { &self.title }
}
+1 -1
View File
@@ -45,7 +45,7 @@ pub use m2ts::M2tsStream;
pub use mkvstream::MkvStream;
pub use network::NetworkStream;
pub use null::NullStream;
pub use resolve::{open_input, open_output, open_pes_input, open_pes_output, parse_url, InputOptions, StreamUrl};
pub use resolve::{input, output, open_input, open_output, parse_url, InputOptions, StreamUrl};
pub use stdio::StdioStream;
use crate::disc::DiscTitle;
+6 -5
View File
@@ -148,7 +148,8 @@ impl Read for NetworkStream {
mod tests {
use super::*;
use crate::disc::{
AudioStream, Codec, ColorSpace, ContentFormat, HdrFormat, Stream, VideoStream,
AudioChannels, AudioStream, Codec, ColorSpace, ContentFormat, FrameRate, HdrFormat,
Resolution, SampleRate, Stream, VideoStream,
};
use std::io::{Read, Write};
use std::net::TcpListener;
@@ -165,8 +166,8 @@ mod tests {
Stream::Video(VideoStream {
pid: 0x1011,
codec: Codec::Hevc,
resolution: "2160p".into(),
frame_rate: "23.976".into(),
resolution: Resolution::R2160p,
frame_rate: FrameRate::F23_976,
hdr: HdrFormat::Hdr10,
color_space: ColorSpace::Bt2020,
secondary: false,
@@ -175,9 +176,9 @@ mod tests {
Stream::Audio(AudioStream {
pid: 0x1100,
codec: Codec::TrueHd,
channels: "7.1".into(),
channels: AudioChannels::Surround71,
language: "eng".into(),
sample_rate: "48kHz".into(),
sample_rate: SampleRate::S48,
secondary: false,
label: "English".into(),
}),
+59 -48
View File
@@ -1,19 +1,15 @@
//! PES output adapters — every output format muxes from PES frames.
//!
//! Each output knows its own format:
//! - M2TS: PES → BD-TS packets → file (via TsMuxer)
//! - Null: discard
//! - Stdio: raw frame data to stdout
//! - Network: PES → BD-TS → TCP (via TsMuxer)
//! PES output streams — each writes its own format from PES frames.
use super::tsmux::TsMuxer;
use crate::disc::DiscTitle;
use crate::pes::{OutputStream, PesFrame};
use crate::pes::PesFrame;
use std::io::{self, Write};
/// M2TS output — PES frames → BD-TS packets → file.
// ── M2TS ────────────────────────────────────────────────────────────────────
pub struct M2tsOutputStream {
muxer: TsMuxer<io::BufWriter<std::fs::File>>,
title: DiscTitle,
}
impl M2tsOutputStream {
@@ -21,79 +17,94 @@ impl M2tsOutputStream {
let file = std::fs::File::create(path)
.map_err(|e| io::Error::new(e.kind(), format!("m2ts://{}: {}", path, e)))?;
let writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
let pids = Self::extract_pids(title);
Ok(Self {
muxer: TsMuxer::new(writer, &pids),
})
}
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()
let pids = extract_pids(title);
Ok(Self { muxer: TsMuxer::new(writer, &pids), title: title.clone() })
}
}
impl OutputStream for M2tsOutputStream {
fn write_frame(&mut self, frame: &PesFrame) -> io::Result<()> {
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_ref()
}
fn finish(&mut self) -> io::Result<()> { self.muxer.finish_ref() }
fn info(&self) -> &DiscTitle { &self.title }
}
/// Null output — discards all frames.
pub struct NullOutputStream;
// ── Null ────────────────────────────────────────────────────────────────────
impl OutputStream for NullOutputStream {
fn write_frame(&mut self, _frame: &PesFrame) -> io::Result<()> { Ok(()) }
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 output — writes raw frame data to stdout.
// ── Stdio ───────────────────────────────────────────────────────────────────
pub struct StdioOutputStream {
writer: io::BufWriter<io::Stdout>,
title: DiscTitle,
}
impl StdioOutputStream {
pub fn new() -> Self {
Self { writer: io::BufWriter::new(io::stdout()) }
pub fn new(title: &DiscTitle) -> Self {
Self { writer: io::BufWriter::new(io::stdout()), title: title.clone() }
}
}
impl OutputStream for StdioOutputStream {
fn write_frame(&mut self, frame: &PesFrame) -> io::Result<()> {
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<()> {
self.writer.write_all(&frame.data)
}
fn finish(&mut self) -> io::Result<()> {
self.writer.flush()
}
fn finish(&mut self) -> io::Result<()> { self.writer.flush() }
fn info(&self) -> &DiscTitle { &self.title }
}
/// Network output — PES frames → BD-TS → TCP.
// ── Network ─────────────────────────────────────────────────────────────────
pub struct NetworkOutputStream {
muxer: TsMuxer<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 writer = io::BufWriter::with_capacity(256 * 1024, stream);
let pids = M2tsOutputStream::extract_pids(title);
Ok(Self {
muxer: TsMuxer::new(writer, &pids),
})
let pids = extract_pids(title);
Ok(Self { muxer: TsMuxer::new(writer, &pids), title: title.clone() })
}
}
impl OutputStream for NetworkOutputStream {
fn write_frame(&mut self, frame: &PesFrame) -> io::Result<()> {
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<()> {
self.muxer.write_frame(frame.track, frame.pts, &frame.data)
}
fn finish(&mut self) -> io::Result<()> {
self.muxer.finish_ref()
}
fn finish(&mut self) -> io::Result<()> { self.muxer.finish_ref() }
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()
}
-1
View File
@@ -82,7 +82,6 @@ impl PsDemuxer {
let mut pos = 0;
while let Some(sc) = find_start_code(&self.buffer, pos) {
if sc + 3 >= self.buffer.len() {
// Not enough bytes to read the start code ID.
break;
+5 -5
View File
@@ -305,7 +305,7 @@ pub struct InputOptions {
// ── PES-based open ──────────────────────────────────────────────────────────
/// Open a PES input stream (produces PES frames).
pub fn open_pes_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::InputStream>> {
pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Iso { ref path } => {
@@ -369,11 +369,11 @@ pub fn open_pes_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crat
}
/// Open a PES output stream (consumes PES frames).
pub fn open_pes_output(
pub fn output(
url: &str,
title: &crate::disc::DiscTitle,
codec_privates: &[Option<Vec<u8>>],
) -> io::Result<Box<dyn crate::pes::OutputStream>> {
) -> io::Result<Box<dyn crate::pes::Stream>> {
let parsed = parse_url(url);
match parsed {
StreamUrl::Mkv { ref path } => {
@@ -393,10 +393,10 @@ pub fn open_pes_output(
Ok(Box::new(super::pesout::NetworkOutputStream::connect(addr, title)?))
}
StreamUrl::Stdio => {
Ok(Box::new(super::pesout::StdioOutputStream::new()))
Ok(Box::new(super::pesout::StdioOutputStream::new(title)))
}
StreamUrl::Null => {
Ok(Box::new(super::pesout::NullOutputStream))
Ok(Box::new(super::pesout::NullOutputStream::new(title)))
}
StreamUrl::Disc { .. } => {
Err(io::Error::new(io::ErrorKind::Unsupported, "disc:// is read-only"))
+20 -25
View File
@@ -1,11 +1,10 @@
//! PES framethe universal intermediate format.
//! Stream — read PES frames in, write PES frames out.
//!
//! Every input stream produces PES frames. Every output stream consumes them.
//! The pipeline just moves frames: input.next_frame() → output.write_frame().
//! A stream is a stream. You read() from it or write() to it.
//! The stream handles its own format internally.
//!
//! A PES frame is one unit of elementary stream data: a video frame,
//! an audio frame, a subtitle packet. It carries a track ID, timestamp,
//! and the raw codec data.
//! disc.read() → PES frame (sectors → decrypt → demux internally)
//! mkv.write(frame) → MKV file (mux internally)
/// One frame of elementary stream data.
#[derive(Debug, Clone)]
@@ -32,27 +31,23 @@ impl PesFrame {
}
}
/// Input stream — produces PES frames from any source.
pub trait InputStream {
/// Get the next frame. Returns None at end of stream.
fn next_frame(&mut self) -> std::io::Result<Option<PesFrame>>;
/// A stream. Read from it or write to it. Not both.
pub trait Stream {
/// Read the next frame. Returns None at end of stream.
fn read(&mut self) -> std::io::Result<Option<PesFrame>>;
/// Stream metadata (tracks, duration, etc).
fn info(&self) -> &crate::disc::DiscTitle;
/// Codec initialization data for a track (SPS/PPS for HEVC, etc).
/// Returns None until enough frames have been parsed.
fn codec_private(&self, track: usize) -> Option<Vec<u8>>;
/// True when codec_private is available for all video tracks.
fn headers_ready(&self) -> bool;
}
/// Output stream — consumes PES frames to any destination.
pub trait OutputStream {
/// Write one frame.
fn write_frame(&mut self, frame: &PesFrame) -> std::io::Result<()>;
/// Write a frame.
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()>;
/// Finalize (flush, write index, close).
fn finish(&mut self) -> std::io::Result<()>;
/// Stream metadata.
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 }
/// True when codec_private is available for all video tracks.
fn headers_ready(&self) -> bool { true }
}
+10 -6
View File
@@ -178,7 +178,7 @@ impl Mt1959 {
fn run_init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
let mut unlocked = false;
for _attempt in 0..6 {
for _attempt in 0..3 {
match self.do_unlock(scsi) {
Ok(_) => {
unlocked = true;
@@ -188,15 +188,17 @@ impl Mt1959 {
return Err(Error::UnlockFailed);
}
Err(_) => {
let ok = if self.mode == MODE_A {
let loaded = if self.mode == MODE_A {
variant_a::load_firmware(self, scsi).is_ok()
} else {
variant_b::load_firmware(self, scsi).is_ok()
};
if ok {
unlocked = true;
break;
if !loaded {
continue;
}
// Firmware upload resets the drive. Give it time to
// fully recover before retrying unlock.
std::thread::sleep(std::time::Duration::from_secs(10));
}
}
}
@@ -319,7 +321,9 @@ impl PlatformDriver for Mt1959 {
fn probe_disc(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
if !self.unlocked {
self.run_init(scsi)?;
// Don't retry init here — if init() failed, probing can't work either.
// Retrying causes repeated USB bus resets on BU40N.
return Ok(());
}
if self.probed {
return Ok(());
+44
View File
@@ -187,6 +187,50 @@ impl UdfFs {
Ok(merged)
}
/// All sector ranges that contain data (metadata + all files including STREAM).
/// For full disc-to-ISO dumps — reads only allocated sectors, skips gaps.
pub fn all_sector_ranges(&self, reader: &mut dyn SectorReader) -> Result<Vec<(u32, u32)>> {
let mut ranges = Vec::new();
// UDF structure sectors
let meta_end = self.metadata_start + self.metadata_sectors;
ranges.push((0, meta_end));
// Walk entire tree including STREAM directories
self.collect_all_file_ranges(reader, &self.root, &mut ranges)?;
// Merge overlapping/adjacent ranges and sort
ranges.sort_by_key(|r| r.0);
let merged = merge_ranges(&ranges);
Ok(merged)
}
fn collect_all_file_ranges(
&self,
reader: &mut dyn SectorReader,
entry: &DirEntry,
ranges: &mut Vec<(u32, u32)>,
) -> Result<()> {
for child in &entry.entries {
if child.is_dir {
self.collect_all_file_ranges(reader, child, ranges)?;
} else {
// Include the ICB sector
ranges.push((self.meta_to_abs(child.meta_lba), 1));
// Include ALL file data extents (large m2ts files have many)
if let Ok(extents) = self.read_icb_extents(reader, child.meta_lba) {
for (data_lba, data_len) in extents {
let abs_start = self.partition_start + data_lba;
let sector_count = (data_len as u64).div_ceil(2048) as u32;
ranges.push((abs_start, sector_count));
}
}
}
}
Ok(())
}
fn collect_file_ranges(
&self,
reader: &mut dyn SectorReader,