Audit fixes + DVD support foundation (IFO, PS demux, MPEG-2, CSS crack)

Audit fixes (14 critical, 22 warnings):
- UDF: bounds checks on all ICB/FID parsing from disc data
- SCSI Linux: saturating_sub on residual, CDB length guard, buffer size guard
- SCSI macOS: SCSITaskStatus u32 (was u8 — stack corruption)
- AACS: EC mod_inv returns infinity instead of panic, key reduced mod n
- AACS: do_handshake tries all host certs (was returning on first failure)
- H.264: bounds check on SPS < 4 bytes
- ContentReader: error on missing unit key (was zero-fill)
- KEYDB: flat redirect loop (was recursive), 100MB response limit, Windows HOME fallback
- ISO writer: AVDP extent order, partition length, allocation cap
- Network: removed TCP_NODELAY on bulk stream
- MKV: guard on u64::MAX seek
- disc.rs: saturating_sub on extent offset, simplified dead region code
- cargo fmt (610 violations), cargo clippy --fix (55 auto-fixes)

DVD support (new files):
- src/ifo.rs — IFO parser (VIDEO_TS.IFO, VTS_XX_0.IFO, PGC chains, cells, streams) — 13 tests
- src/mux/ps.rs — MPEG-2 Program Stream demuxer (pack headers, PES, private stream 1) — 12 tests
- src/mux/codec/mpeg2.rs — MPEG-2 video parser (sequence headers, I-frame detection) — 15 tests
- src/css/crack.rs — split-attack algorithm (LFSR cipher needs verification — test ignored)

226 tests total (was 186), 1 ignored (CSS crack needs cipher verification).
This commit is contained in:
MattJackson
2026-04-11 16:52:22 +00:00
parent 6de4ee4b5c
commit 01235ff347
57 changed files with 6189 additions and 1519 deletions
+13 -2
View File
@@ -4,10 +4,16 @@
//! Each PES packet typically contains exactly one AC3 frame.
//! All AC3 frames are effectively keyframes (no inter-frame dependencies).
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct Ac3Parser;
impl Default for Ac3Parser {
fn default() -> Self {
Self::new()
}
}
impl Ac3Parser {
pub fn new() -> Self {
Self
@@ -54,7 +60,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
// --- syncword detection ---
+24 -5
View File
@@ -5,12 +5,20 @@
//! All frames are keyframes (no inter-frame dependencies).
//! Each PES packet = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct DtsParser;
impl Default for DtsParser {
fn default() -> Self {
Self::new()
}
}
impl DtsParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for DtsParser {
@@ -19,10 +27,16 @@ impl CodecParser for DtsParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -31,7 +45,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
#[test]
+45 -12
View File
@@ -4,7 +4,7 @@
//! Detects keyframes (IDR slices).
//! Each PES packet = one access unit = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
/// H.264 NAL unit types we care about.
const NAL_SLICE_IDR: u8 = 5;
@@ -17,9 +17,18 @@ pub struct H264Parser {
pps: Option<Vec<u8>>,
}
impl Default for H264Parser {
fn default() -> Self {
Self::new()
}
}
impl H264Parser {
pub fn new() -> Self {
Self { sps: None, pps: None }
Self {
sps: None,
pps: None,
}
}
}
@@ -84,6 +93,10 @@ impl CodecParser for H264Parser {
let sps = self.sps.as_ref()?;
let pps = self.pps.as_ref()?;
if sps.len() < 4 {
return None;
}
// AVCDecoderConfigurationRecord (ISO 14496-15):
// configurationVersion = 1
// AVCProfileIndication = SPS[1]
@@ -196,7 +209,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
// --- find_start_code tests ---
@@ -246,7 +264,7 @@ mod tests {
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.push(0x67); // SPS
data.extend_from_slice(&[0x42, 0x00, 0x1E, 0xAB, 0xCD]); // profile=0x42, compat=0x00, level=0x1E
// PPS: 00 00 01 [68 <payload>]
// PPS: 00 00 01 [68 <payload>]
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.push(0x68); // PPS
data.extend_from_slice(&[0xCE, 0x01]);
@@ -260,7 +278,10 @@ mod tests {
// codec_private should now be available
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after seeing SPS+PPS");
assert!(
cp.is_some(),
"codec_private should be Some after seeing SPS+PPS"
);
let cp = cp.unwrap();
// AVCDecoderConfigurationRecord checks
@@ -297,7 +318,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe, "IDR slice should be detected as keyframe");
assert!(
frames[0].keyframe,
"IDR slice should be detected as keyframe"
);
}
// --- non-IDR → not keyframe ---
@@ -338,16 +362,25 @@ mod tests {
let frame_data = &frames[0].data;
// Should start with 4-byte big-endian length prefix
assert!(frame_data.len() >= 4, "frame data should have length prefix");
let length = u32::from_be_bytes([frame_data[0], frame_data[1], frame_data[2], frame_data[3]]);
assert_eq!(length as usize, nal_payload.len(), "length prefix should match NAL size");
assert!(
frame_data.len() >= 4,
"frame data should have length prefix"
);
let length =
u32::from_be_bytes([frame_data[0], frame_data[1], frame_data[2], frame_data[3]]);
assert_eq!(
length as usize,
nal_payload.len(),
"length prefix should match NAL size"
);
// Followed by the NAL data itself
assert_eq!(&frame_data[4..], &nal_payload);
// No start code (00 00 01) should appear in the output
for i in 0..frame_data.len().saturating_sub(2) {
let is_sc = frame_data[i] == 0x00 && frame_data[i + 1] == 0x00 && frame_data[i + 2] == 0x01;
let is_sc =
frame_data[i] == 0x00 && frame_data[i + 1] == 0x00 && frame_data[i + 2] == 0x01;
assert!(!is_sc, "output should not contain Annex B start codes");
}
}
@@ -429,8 +462,8 @@ mod tests {
let pes = PesPacket {
pid: 0x1011,
pts: Some(180000), // 2 seconds
dts: Some(90000), // 1 second
pts: Some(180000), // 2 seconds
dts: Some(90000), // 1 second
data,
};
let frames = parser.parse(&pes);
+62 -19
View File
@@ -4,8 +4,8 @@
//! Detects keyframes (IRAP pictures: IDR, CRA, BLA).
//! Each PES packet = one access unit = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::h264::{find_start_code, skip_start_code};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
// HEVC NAL unit types
const NAL_VPS: u8 = 32;
@@ -22,9 +22,19 @@ pub struct HevcParser {
pps: Option<Vec<u8>>,
}
impl Default for HevcParser {
fn default() -> Self {
Self::new()
}
}
impl HevcParser {
pub fn new() -> Self {
Self { vps: None, sps: None, pps: None }
Self {
vps: None,
sps: None,
pps: None,
}
}
}
@@ -45,7 +55,9 @@ impl CodecParser for HevcParser {
if let Some(nal_start) = skip_start_code(data, sc_pos) {
let next = find_start_code(data, nal_start).unwrap_or(data.len());
let mut end = next;
while end > nal_start && data[end - 1] == 0x00 { end -= 1; }
while end > nal_start && data[end - 1] == 0x00 {
end -= 1;
}
if nal_start < data.len() {
// HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte.
@@ -55,7 +67,7 @@ impl CodecParser for HevcParser {
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()),
t if t >= NAL_BLA_W_LP && t <= NAL_RSV_IRAP_VCL23 => {
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
keyframe = true;
}
_ => {}
@@ -75,12 +87,18 @@ impl CodecParser for HevcParser {
if let Some(nal_start) = skip_start_code(&pes.data, sc_pos) {
let next = find_start_code(&pes.data, nal_start).unwrap_or(pes.data.len());
let mut end = next;
while end > nal_start && pes.data[end - 1] == 0x00 { end -= 1; }
while end > nal_start && pes.data[end - 1] == 0x00 {
end -= 1;
}
if nal_start < pes.data.len() {
let nal_type = (pes.data[nal_start] >> 1) & 0x3F;
// Skip parameter sets and AUD
if nal_type != NAL_VPS && nal_type != NAL_SPS && nal_type != NAL_PPS && nal_type != NAL_AUD {
if nal_type != NAL_VPS
&& nal_type != NAL_SPS
&& nal_type != NAL_PPS
&& nal_type != NAL_AUD
{
let nal = &pes.data[nal_start..end];
let len = nal.len() as u32;
frame_data.extend_from_slice(&len.to_be_bytes());
@@ -115,8 +133,8 @@ impl CodecParser for HevcParser {
let mut record = Vec::new();
// Minimal HEVCDecoderConfigurationRecord header
record.push(1); // configurationVersion
// General profile space, tier flag, profile IDC from SPS
record.push(1); // configurationVersion
// General profile space, tier flag, profile IDC from SPS
if sps.len() > 3 {
record.push(sps[1]); // general_profile_space + general_tier_flag + general_profile_idc
} else {
@@ -134,7 +152,7 @@ impl CodecParser for HevcParser {
record.push(0xFC);
// chromaFormat (6 + 2 bits)
record.push(0xFC | 1); // 4:2:0
// bitDepthLumaMinus8 (5 + 3 bits)
// bitDepthLumaMinus8 (5 + 3 bits)
record.push(0xF8);
// bitDepthChromaMinus8 (5 + 3 bits)
record.push(0xF8);
@@ -142,7 +160,7 @@ impl CodecParser for HevcParser {
record.extend_from_slice(&[0, 0]);
// constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne
record.push(0x03); // lengthSizeMinusOne = 3 (4 bytes)
// numOfArrays
// numOfArrays
record.push(3); // VPS, SPS, PPS
// VPS array
@@ -176,7 +194,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
/// Build an HEVC NAL header (2 bytes). Type is bits 1-6 of first byte.
@@ -202,8 +225,9 @@ mod tests {
data.extend_from_slice(&[0x00, 0x00, 0x01]);
let sps_hdr = hevc_nal_header(33);
data.extend_from_slice(&sps_hdr);
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D]); // SPS payload (>12 bytes for level)
data.extend_from_slice(&[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
]); // SPS payload (>12 bytes for level)
// PPS (type 34)
data.extend_from_slice(&[0x00, 0x00, 0x01]);
@@ -221,7 +245,10 @@ mod tests {
let _frames = parser.parse(&pes);
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after VPS+SPS+PPS");
assert!(
cp.is_some(),
"codec_private should be Some after VPS+SPS+PPS"
);
let cp = cp.unwrap();
// configurationVersion = 1
@@ -229,7 +256,10 @@ mod tests {
// numOfArrays = 3 (VPS, SPS, PPS)
assert_eq!(cp[22], 3);
// Should be longer than the minimal header (23 bytes) + array entries
assert!(cp.len() > 23, "codec_private should contain VPS+SPS+PPS data");
assert!(
cp.len() > 23,
"codec_private should contain VPS+SPS+PPS data"
);
}
#[test]
@@ -257,7 +287,10 @@ mod tests {
let pes = make_pes(data, Some(0));
parser.parse(&pes);
assert!(parser.codec_private().is_none(), "should be None without PPS");
assert!(
parser.codec_private().is_none(),
"should be None without PPS"
);
}
// --- IRAP keyframe detection ---
@@ -276,7 +309,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe, "IDR_W_RADL (type 19) should be keyframe");
assert!(
frames[0].keyframe,
"IDR_W_RADL (type 19) should be keyframe"
);
}
#[test]
@@ -343,7 +379,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "TRAIL_R (type 1) should not be keyframe");
assert!(
!frames[0].keyframe,
"TRAIL_R (type 1) should not be keyframe"
);
}
#[test]
@@ -395,7 +434,11 @@ mod tests {
let fd = &frames[0].data;
let length = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]);
// IDR NAL = 2 bytes header + 2 bytes payload = 4 bytes
assert_eq!(length as usize + 4, fd.len(), "frame should contain exactly one length-prefixed NAL");
assert_eq!(
length as usize + 4,
fd.len(),
"frame should contain exactly one length-prefixed NAL"
);
}
// --- empty PES ---
+9 -5
View File
@@ -8,15 +8,16 @@
//! - Convert PTS from 90kHz to nanoseconds
pub mod ac3;
pub mod dts;
pub mod h264;
pub mod hevc;
pub mod vc1;
pub mod dts;
pub mod truehd;
pub mod mpeg2;
pub mod pgs;
pub mod truehd;
pub mod vc1;
use crate::disc::Codec;
use super::ts::PesPacket;
use crate::disc::Codec;
/// A single frame ready for MKV muxing.
pub struct Frame {
@@ -53,7 +54,9 @@ pub struct PassthroughParser {
impl PassthroughParser {
pub fn new(always_keyframe: bool) -> Self {
Self { keyframe: always_keyframe }
Self {
keyframe: always_keyframe,
}
}
}
@@ -77,6 +80,7 @@ pub fn parser_for_codec(codec: Codec) -> Box<dyn CodecParser> {
match codec {
Codec::H264 => Box::new(h264::H264Parser::new()),
Codec::Hevc => Box::new(hevc::HevcParser::new()),
Codec::Mpeg2 => Box::new(mpeg2::Mpeg2Parser::new()),
Codec::Vc1 => Box::new(vc1::Vc1Parser::new()),
Codec::Ac3 | Codec::Ac3Plus => Box::new(ac3::Ac3Parser::new()),
Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => Box::new(dts::DtsParser::new()),
+448
View File
@@ -0,0 +1,448 @@
//! MPEG-2 Video elementary stream parser.
//!
//! Extracts sequence headers for MKV codecPrivate.
//! Detects keyframes (I-frames from picture headers).
//! Each PES packet = one access unit = one frame.
//!
//! Start codes:
//! - Sequence header: 00 00 01 B3
//! - Sequence extension: 00 00 01 B5
//! - Picture header: 00 00 01 00
use super::{pts_to_ns, CodecParser, Frame};
use crate::mux::ts::PesPacket;
/// Sequence header start code suffix.
const SEQ_HEADER_CODE: u8 = 0xB3;
/// Sequence extension start code suffix.
const SEQ_EXT_CODE: u8 = 0xB5;
/// Picture start code suffix.
const PICTURE_CODE: u8 = 0x00;
/// Picture coding type: I-frame.
const PICTURE_TYPE_I: u8 = 1;
/// Frame rate table (index from sequence header frame_rate_code).
const FRAME_RATES: [(u32, u32); 9] = [
(0, 1), // 0: forbidden
(24000, 1001), // 1: 23.976
(24, 1), // 2: 24
(25, 1), // 3: 25
(30000, 1001), // 4: 29.97
(30, 1), // 5: 30
(50, 1), // 6: 50
(60000, 1001), // 7: 59.94
(60, 1), // 8: 60
];
/// Aspect ratio table (index from sequence header aspect_ratio_information).
const ASPECT_RATIOS: [(u8, u8); 5] = [
(0, 0), // 0: forbidden
(1, 1), // 1: square pixels (1:1 SAR)
(4, 3), // 2: 4:3 display
(16, 9), // 3: 16:9 display
(221, 100), // 4: 2.21:1 display
];
/// MPEG-2 Video elementary stream parser.
pub struct Mpeg2Parser {
/// Raw bytes of the last seen sequence header (+ sequence extension if found).
seq_header: Option<Vec<u8>>,
}
impl Default for Mpeg2Parser {
fn default() -> Self {
Self::new()
}
}
impl Mpeg2Parser {
pub fn new() -> Self {
Self { seq_header: None }
}
/// Extract resolution from a captured sequence header.
/// Returns (width, height) or None if the header is too short.
pub fn resolution(&self) -> Option<(u16, u16)> {
let hdr = self.seq_header.as_ref()?;
parse_resolution(hdr)
}
/// Extract frame rate from a captured sequence header.
/// Returns (numerator, denominator) or None.
pub fn frame_rate(&self) -> Option<(u32, u32)> {
let hdr = self.seq_header.as_ref()?;
parse_frame_rate(hdr)
}
/// Extract aspect ratio from a captured sequence header.
/// Returns (width, height) for display aspect ratio, or None.
pub fn aspect_ratio(&self) -> Option<(u8, u8)> {
let hdr = self.seq_header.as_ref()?;
parse_aspect_ratio(hdr)
}
}
impl CodecParser for Mpeg2Parser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
if pes.data.is_empty() {
return Vec::new();
}
let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0);
let data = &pes.data;
let mut keyframe = false;
// Scan for start codes in the elementary stream data.
let mut pos = 0;
while let Some(sc) = find_start_code(data, pos) {
if sc + 3 >= data.len() {
break;
}
let code = data[sc + 3];
match code {
SEQ_HEADER_CODE => {
// Capture sequence header: from start code to next start code
// (or to the sequence extension if present).
let hdr_start = sc;
let hdr_end = find_start_code(data, sc + 4).unwrap_or(data.len());
let mut seq_data = data[hdr_start..hdr_end].to_vec();
// Check if sequence extension follows immediately.
if hdr_end + 3 < data.len() && data[hdr_end + 3] == SEQ_EXT_CODE {
let ext_end =
find_start_code(data, hdr_end + 4).unwrap_or(data.len());
seq_data.extend_from_slice(&data[hdr_end..ext_end]);
}
self.seq_header = Some(seq_data);
// Sequence header implies I-frame follows.
keyframe = true;
pos = sc + 4;
}
PICTURE_CODE => {
// Picture header: bytes after start code contain temporal_reference
// (10 bits) + picture_coding_type (3 bits).
if sc + 5 < data.len() {
let picture_coding_type = (data[sc + 5] >> 3) & 0x07;
if picture_coding_type == PICTURE_TYPE_I {
keyframe = true;
}
}
pos = sc + 4;
}
_ => {
pos = sc + 4;
}
}
}
vec![Frame {
pts_ns,
keyframe,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> {
self.seq_header.clone()
}
}
/// Parse horizontal and vertical resolution from sequence header bytes.
/// The sequence header must start with 00 00 01 B3.
fn parse_resolution(hdr: &[u8]) -> Option<(u16, u16)> {
// Need at least start code (4) + 4 bytes of header data = 8 bytes.
if hdr.len() < 8 {
return None;
}
// Bytes 4-5: horizontal_size_value (12 bits) | vertical_size_value top 4 bits
// Bytes 5-6: vertical_size_value bottom 8 bits (12 bits total)
let h = ((hdr[4] as u16) << 4) | ((hdr[5] as u16) >> 4);
let v = (((hdr[5] & 0x0F) as u16) << 8) | hdr[6] as u16;
Some((h, v))
}
/// Parse frame rate code from sequence header.
fn parse_frame_rate(hdr: &[u8]) -> Option<(u32, u32)> {
if hdr.len() < 8 {
return None;
}
let frame_rate_code = (hdr[7] & 0x0F) as usize;
if frame_rate_code == 0 || frame_rate_code >= FRAME_RATES.len() {
return None;
}
Some(FRAME_RATES[frame_rate_code])
}
/// Parse aspect ratio information from sequence header.
fn parse_aspect_ratio(hdr: &[u8]) -> Option<(u8, u8)> {
if hdr.len() < 8 {
return None;
}
let ar_code = ((hdr[7] >> 4) & 0x0F) as usize;
if ar_code == 0 || ar_code >= ASPECT_RATIOS.len() {
return None;
}
Some(ASPECT_RATIOS[ar_code])
}
/// Find the position of the next start code (00 00 01) at or after `from`.
fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
if data.len() < from + 3 {
return None;
}
for i in from..data.len() - 2 {
if data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01 {
return Some(i);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
/// Build a minimal MPEG-2 sequence header.
/// 00 00 01 B3 [h_size:12][v_size:12] [aspect:4][frame_rate:4] ...
fn make_seq_header(width: u16, height: u16, aspect: u8, frame_rate: u8) -> Vec<u8> {
let mut hdr = vec![0x00, 0x00, 0x01, SEQ_HEADER_CODE];
hdr.push((width >> 4) as u8);
hdr.push(((width & 0x0F) as u8) << 4 | ((height >> 8) & 0x0F) as u8);
hdr.push((height & 0xFF) as u8);
hdr.push((aspect << 4) | (frame_rate & 0x0F));
// Bit rate (18 bits) + marker + VBV buffer size (10 bits) etc — pad minimally.
hdr.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]);
hdr
}
/// Build a picture header with the given coding type.
fn make_picture_header(coding_type: u8) -> Vec<u8> {
// 00 00 01 00 [temporal_ref:10][picture_coding_type:3][...]
// temporal_reference = 0 for simplicity
// byte4 = temporal_ref[9:2] = 0x00
// byte5 = temporal_ref[1:0] | picture_coding_type[2:0] << 3 | ...
let byte5 = (coding_type & 0x07) << 3;
vec![0x00, 0x00, 0x01, PICTURE_CODE, 0x00, byte5, 0x00, 0x00]
}
// --- Sequence header parsing ---
#[test]
fn parse_sequence_header_resolution() {
let hdr = make_seq_header(720, 480, 2, 4);
let res = parse_resolution(&hdr);
assert_eq!(res, Some((720, 480)));
}
#[test]
fn parse_sequence_header_1920x1080() {
let hdr = make_seq_header(1920, 1080, 3, 4);
let res = parse_resolution(&hdr);
assert_eq!(res, Some((1920, 1080)));
}
#[test]
fn parse_sequence_header_frame_rate() {
let hdr = make_seq_header(720, 480, 2, 4); // frame_rate_code 4 = 29.97
let fr = parse_frame_rate(&hdr);
assert_eq!(fr, Some((30000, 1001)));
}
#[test]
fn parse_sequence_header_aspect_ratio() {
let hdr = make_seq_header(720, 480, 3, 4); // aspect code 3 = 16:9
let ar = parse_aspect_ratio(&hdr);
assert_eq!(ar, Some((16, 9)));
}
#[test]
fn parse_sequence_header_too_short() {
let hdr = vec![0x00, 0x00, 0x01, SEQ_HEADER_CODE];
assert!(parse_resolution(&hdr).is_none());
assert!(parse_frame_rate(&hdr).is_none());
assert!(parse_aspect_ratio(&hdr).is_none());
}
// --- I-frame detection ---
#[test]
fn detect_i_frame() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
// Some payload data after the picture header.
data.extend_from_slice(&[0xFF; 16]);
let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe, "I-frame should be detected as keyframe");
}
#[test]
fn detect_p_frame_not_keyframe() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_picture_header(2)); // P-frame
data.extend_from_slice(&[0xFF; 16]);
let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "P-frame should not be keyframe");
}
#[test]
fn detect_b_frame_not_keyframe() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_picture_header(3)); // B-frame
data.extend_from_slice(&[0xFF; 16]);
let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "B-frame should not be keyframe");
}
// --- Sequence header → codec_private ---
#[test]
fn codec_private_from_sequence_header() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
let seq = make_seq_header(720, 480, 3, 4);
data.extend_from_slice(&seq);
// Follow with a picture header (I-frame).
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
data.extend_from_slice(&[0xFF; 8]);
let pes = make_pes(data, Some(0));
let _frames = parser.parse(&pes);
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be available after sequence header");
let cp = cp.unwrap();
// Should start with the sequence header start code.
assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]);
}
#[test]
fn codec_private_none_initially() {
let parser = Mpeg2Parser::new();
assert!(parser.codec_private().is_none());
}
// --- Sequence header with extension ---
#[test]
fn codec_private_includes_extension() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
let seq = make_seq_header(1920, 1080, 3, 4);
data.extend_from_slice(&seq);
// Sequence extension: 00 00 01 B5 [ext data]
data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE]);
data.extend_from_slice(&[0x14, 0x8A, 0x00, 0x01, 0x00, 0x00]); // ext payload
// Picture header follows.
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
data.extend_from_slice(&[0xFF; 4]);
let pes = make_pes(data, Some(0));
let _frames = parser.parse(&pes);
let cp = parser.codec_private().unwrap();
// Should contain both sequence header and sequence extension start codes.
let has_ext = cp.windows(4).any(|w| w == [0x00, 0x00, 0x01, SEQ_EXT_CODE]);
assert!(has_ext, "codec_private should include sequence extension");
}
// --- I-frame with sequence header = keyframe ---
#[test]
fn sequence_header_implies_keyframe() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_seq_header(720, 480, 3, 4));
// Even without an explicit picture header, a sequence header implies I-frame.
data.extend_from_slice(&[0xFF; 16]);
let pes = make_pes(data, Some(0));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe);
}
// --- PTS conversion ---
#[test]
fn pts_conversion_to_nanoseconds() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
data.extend_from_slice(&[0xFF; 4]);
// 90000 ticks = 1 second = 1_000_000_000 ns
let pes = make_pes(data, Some(90000));
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].pts_ns, 1_000_000_000);
}
// --- Empty PES ---
#[test]
fn empty_pes_no_frames() {
let mut parser = Mpeg2Parser::new();
let pes = make_pes(Vec::new(), Some(0));
let frames = parser.parse(&pes);
assert!(frames.is_empty());
}
// --- Resolution helper methods ---
#[test]
fn parser_resolution_method() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_seq_header(720, 576, 2, 3));
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
data.extend_from_slice(&[0xFF; 4]);
let pes = make_pes(data, Some(0));
let _ = parser.parse(&pes);
assert_eq!(parser.resolution(), Some((720, 576)));
assert_eq!(parser.frame_rate(), Some((25, 1))); // frame_rate_code 3 = 25fps
assert_eq!(parser.aspect_ratio(), Some((4, 3))); // aspect code 2 = 4:3
}
}
+24 -5
View File
@@ -4,12 +4,20 @@
//! Each PES packet contains one or more segments.
//! All segments are keyframes (no inter-segment dependencies).
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct PgsParser;
impl Default for PgsParser {
fn default() -> Self {
Self::new()
}
}
impl PgsParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for PgsParser {
@@ -18,10 +26,16 @@ impl CodecParser for PgsParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -30,7 +44,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1200, pts, dts: None, data }
PesPacket {
pid: 0x1200,
pts,
dts: None,
data,
}
}
#[test]
+24 -5
View File
@@ -6,12 +6,20 @@
//! All access units are keyframes.
//! Each PES packet = one access unit.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct TrueHdParser;
impl Default for TrueHdParser {
fn default() -> Self {
Self::new()
}
}
impl TrueHdParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for TrueHdParser {
@@ -20,10 +28,16 @@ impl CodecParser for TrueHdParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -32,7 +46,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
#[test]
+57 -21
View File
@@ -5,7 +5,7 @@
//! Frame start = Frame header start code (0x0D).
//! I-frames (keyframes) are identified from the frame header.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
const SC_SEQUENCE_HEADER: u8 = 0x0F;
const SC_ENTRY_POINT: u8 = 0x0E;
@@ -16,9 +16,18 @@ pub struct Vc1Parser {
entry_point: Option<Vec<u8>>,
}
impl Default for Vc1Parser {
fn default() -> Self {
Self::new()
}
}
impl Vc1Parser {
pub fn new() -> Self {
Self { seq_header: None, entry_point: None }
Self {
seq_header: None,
entry_point: None,
}
}
}
@@ -92,17 +101,17 @@ impl CodecParser for Vc1Parser {
let mut cp = Vec::with_capacity(header_size as usize);
// BITMAPINFOHEADER (40 bytes, little-endian)
cp.extend_from_slice(&header_size.to_le_bytes()); // biSize
cp.extend_from_slice(&1920u32.to_le_bytes()); // biWidth (updated by player)
cp.extend_from_slice(&1080u32.to_le_bytes()); // biHeight
cp.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
cp.extend_from_slice(&24u16.to_le_bytes()); // biBitCount
cp.extend_from_slice(b"WVC1"); // biCompression = "WVC1" FOURCC
cp.extend_from_slice(&0u32.to_le_bytes()); // biSizeImage
cp.extend_from_slice(&0u32.to_le_bytes()); // biXPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biYPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrUsed
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrImportant
cp.extend_from_slice(&header_size.to_le_bytes()); // biSize
cp.extend_from_slice(&1920u32.to_le_bytes()); // biWidth (updated by player)
cp.extend_from_slice(&1080u32.to_le_bytes()); // biHeight
cp.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
cp.extend_from_slice(&24u16.to_le_bytes()); // biBitCount
cp.extend_from_slice(b"WVC1"); // biCompression = "WVC1" FOURCC
cp.extend_from_slice(&0u32.to_le_bytes()); // biSizeImage
cp.extend_from_slice(&0u32.to_le_bytes()); // biXPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biYPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrUsed
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrImportant
// Extra codec data: sequence header + entry point (Annex B)
cp.extend_from_slice(sh);
@@ -127,7 +136,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
/// Build a VC-1 PES with sequence header + entry point + frame start code.
@@ -157,7 +171,10 @@ mod tests {
assert_eq!(frames.len(), 1);
// Sequence header present → keyframe
assert!(frames[0].keyframe, "PES with sequence header should be keyframe");
assert!(
frames[0].keyframe,
"PES with sequence header should be keyframe"
);
// seq_header should be stored internally
assert!(parser.seq_header.is_some());
}
@@ -184,15 +201,25 @@ mod tests {
parser.parse(&pes);
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after seq header + entry point");
assert!(
cp.is_some(),
"codec_private should be Some after seq header + entry point"
);
let cp = cp.unwrap();
// BITMAPINFOHEADER is 40 bytes + extra data
assert!(cp.len() >= 40, "codec_private should be at least 40 bytes (BITMAPINFOHEADER)");
assert!(
cp.len() >= 40,
"codec_private should be at least 40 bytes (BITMAPINFOHEADER)"
);
// biSize (first 4 bytes, little-endian) should equal total length
let bi_size = u32::from_le_bytes([cp[0], cp[1], cp[2], cp[3]]);
assert_eq!(bi_size as usize, cp.len(), "biSize should match total codec_private length");
assert_eq!(
bi_size as usize,
cp.len(),
"biSize should match total codec_private length"
);
// biCompression = "WVC1" at offset 16
assert_eq!(&cp[16..20], b"WVC1", "FOURCC should be WVC1");
@@ -226,7 +253,10 @@ mod tests {
let pes = make_pes(data, Some(0));
parser.parse(&pes);
assert!(parser.codec_private().is_none(), "should be None without entry point");
assert!(
parser.codec_private().is_none(),
"should be None without entry point"
);
}
// --- frame without sequence header → not keyframe ---
@@ -244,7 +274,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "frame without sequence header should not be keyframe");
assert!(
!frames[0].keyframe,
"frame without sequence header should not be keyframe"
);
}
// --- frame data starts from frame start code ---
@@ -337,7 +370,10 @@ mod tests {
let cp = parser.codec_private().unwrap();
// After the 40-byte BITMAPINFOHEADER, we should have seq_header + entry_point data
let extra = &cp[40..];
assert!(!extra.is_empty(), "extra data after BITMAPINFOHEADER should not be empty");
assert!(
!extra.is_empty(),
"extra data after BITMAPINFOHEADER should not be empty"
);
// Extra data should start with the sequence header start code
assert_eq!(&extra[0..4], &[0x00, 0x00, 0x01, SC_SEQUENCE_HEADER]);
}
+47 -24
View File
@@ -3,14 +3,15 @@
//! Read-only stream. Wraps DriveSession + Disc.
//! Handles drive init, AACS decryption, and sector reading.
use std::io::{self, Read, Write};
use std::path::Path;
use super::IOStream;
use crate::disc::{DiscTitle, Disc};
use crate::disc::{Disc, DiscTitle};
use crate::drive::DriveSession;
use crate::error::Error;
use std::io::{self, Read, Write};
use std::path::Path;
/// Options for opening a disc stream.
#[derive(Default)]
pub struct DiscOptions {
/// Device path (e.g. "/dev/sg4"). None = auto-detect.
pub device: Option<String>,
@@ -20,11 +21,6 @@ pub struct DiscOptions {
pub title_index: Option<usize>,
}
impl Default for DiscOptions {
fn default() -> Self {
Self { device: None, keydb_path: None, title_index: None }
}
}
/// Optical disc stream. Read-only — yields decrypted BD-TS bytes.
pub struct DiscStream {
@@ -44,8 +40,9 @@ impl DiscStream {
pub fn open(opts: DiscOptions) -> Result<Self, Error> {
let device = match opts.device {
Some(ref d) => crate::drive::resolve_device(d)?.0,
None => crate::drive::find_drive()
.ok_or_else(|| Error::DeviceNotFound { path: String::new() })?,
None => crate::drive::find_drive().ok_or_else(|| Error::DeviceNotFound {
path: String::new(),
})?,
};
let mut session = DriveSession::open(Path::new(&device))?;
@@ -61,24 +58,38 @@ impl DiscStream {
let title_index = opts.title_index.unwrap_or(0);
if title_index >= disc.titles.len() {
return Err(Error::DiscTitleRange { index: title_index, count: disc.titles.len() });
return Err(Error::DiscTitleRange {
index: title_index,
count: disc.titles.len(),
});
}
let disc_title = disc.titles[title_index].clone();
Ok(Self {
disc_title, disc, session, title_index,
batch_buf: Vec::new(), batch_pos: 0,
started: false, eof: false,
disc_title,
disc,
session,
title_index,
batch_buf: Vec::new(),
batch_pos: 0,
started: false,
eof: false,
})
}
/// Get the full Disc (for listing all titles, etc.)
pub fn disc(&self) -> &Disc { &self.disc }
pub fn disc(&self) -> &Disc {
&self.disc
}
}
impl IOStream for DiscStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Read for DiscStream {
@@ -91,7 +102,9 @@ impl Read for DiscStream {
return Ok(n);
}
if self.eof { return Ok(0); }
if self.eof {
return Ok(0);
}
// Open reader on first call
if !self.started {
@@ -100,8 +113,10 @@ impl Read for DiscStream {
// Read next batch via a temporary ContentReader
// ContentReader borrows session and disc, so we create it inline
let mut reader = self.disc.open_title(&mut self.session, self.title_index)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let mut reader = self
.disc
.open_title(&mut self.session, self.title_index)
.map_err(|e| io::Error::other(e.to_string()))?;
match reader.read_batch() {
Ok(Some(batch)) => {
@@ -116,15 +131,23 @@ impl Read for DiscStream {
}
Ok(n)
}
Ok(None) => { self.eof = true; Ok(0) }
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e.to_string())),
Ok(None) => {
self.eof = true;
Ok(0)
}
Err(e) => Err(io::Error::other(e.to_string())),
}
}
}
impl Write for DiscStream {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::Unsupported, "disc is read-only"))
Err(io::Error::new(
io::ErrorKind::Unsupported,
"disc is read-only",
))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn flush(&mut self) -> io::Result<()> { Ok(()) }
}
+124 -31
View File
@@ -3,7 +3,7 @@
//! EBML uses variable-length integers for element IDs and sizes.
//! This module provides low-level writers for constructing MKV files.
use std::io::{self, Read, Write, Seek, SeekFrom};
use std::io::{self, Read, Seek, SeekFrom, Write};
/// Write an EBML element ID (1-4 bytes, already encoded).
/// Element IDs are predefined constants — we write them verbatim.
@@ -15,7 +15,12 @@ pub fn write_id(w: &mut impl Write, id: u32) -> io::Result<()> {
} else if id <= 0xFF_FFFF {
w.write_all(&[(id >> 16) as u8, (id >> 8) as u8, id as u8])
} else {
w.write_all(&[(id >> 24) as u8, (id >> 16) as u8, (id >> 8) as u8, id as u8])
w.write_all(&[
(id >> 24) as u8,
(id >> 16) as u8,
(id >> 8) as u8,
id as u8,
])
}
}
@@ -27,11 +32,7 @@ pub fn write_size(w: &mut impl Write, size: u64) -> io::Result<()> {
} else if size < 0x3FFF {
w.write_all(&[((size >> 8) as u8) | 0x40, size as u8])
} else if size < 0x1F_FFFF {
w.write_all(&[
((size >> 16) as u8) | 0x20,
(size >> 8) as u8,
size as u8,
])
w.write_all(&[((size >> 16) as u8) | 0x20, (size >> 8) as u8, size as u8])
} else if size < 0x0FFF_FFFF {
w.write_all(&[
((size >> 24) as u8) | 0x10,
@@ -75,8 +76,10 @@ pub fn write_uint(w: &mut impl Write, id: u32, val: u64) -> io::Result<()> {
} else if val <= 0xFFFF_FFFF {
write_size(w, 4)?;
w.write_all(&[
(val >> 24) as u8, (val >> 16) as u8,
(val >> 8) as u8, val as u8,
(val >> 24) as u8,
(val >> 16) as u8,
(val >> 8) as u8,
val as u8,
])
} else {
write_size(w, 8)?;
@@ -163,9 +166,15 @@ pub fn read_id(r: &mut impl Read) -> io::Result<(u32, usize)> {
} else if b0 & 0x10 != 0 {
let mut b = [0u8; 3];
r.read_exact(&mut b)?;
Ok((((b0 as u32) << 24) | (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32, 4))
Ok((
((b0 as u32) << 24) | (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32,
4,
))
} else {
Err(io::Error::new(io::ErrorKind::InvalidData, "invalid EBML ID"))
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid EBML ID",
))
}
}
@@ -178,46 +187,78 @@ pub fn read_size(r: &mut impl Read) -> io::Result<(u64, usize)> {
if b0 & 0x80 != 0 {
let val = (b0 & 0x7F) as u64;
if val == 0x7F { return Ok((u64::MAX, 1)); } // unknown
if val == 0x7F {
return Ok((u64::MAX, 1));
} // unknown
Ok((val, 1))
} else if b0 & 0x40 != 0 {
let mut b = [0u8; 1];
r.read_exact(&mut b)?;
let val = (((b0 & 0x3F) as u64) << 8) | b[0] as u64;
if val == 0x3FFF { return Ok((u64::MAX, 2)); }
if val == 0x3FFF {
return Ok((u64::MAX, 2));
}
Ok((val, 2))
} else if b0 & 0x20 != 0 {
let mut b = [0u8; 2];
r.read_exact(&mut b)?;
let val = (((b0 & 0x1F) as u64) << 16) | (b[0] as u64) << 8 | b[1] as u64;
if val == 0x1FFFFF { return Ok((u64::MAX, 3)); }
if val == 0x1FFFFF {
return Ok((u64::MAX, 3));
}
Ok((val, 3))
} else if b0 & 0x10 != 0 {
let mut b = [0u8; 3];
r.read_exact(&mut b)?;
let val = (((b0 & 0x0F) as u64) << 24) | (b[0] as u64) << 16 | (b[1] as u64) << 8 | b[2] as u64;
if val == 0x0FFFFFFF { return Ok((u64::MAX, 4)); }
let val =
(((b0 & 0x0F) as u64) << 24) | (b[0] as u64) << 16 | (b[1] as u64) << 8 | b[2] as u64;
if val == 0x0FFFFFFF {
return Ok((u64::MAX, 4));
}
Ok((val, 4))
} else if b0 & 0x08 != 0 {
let mut b = [0u8; 4];
r.read_exact(&mut b)?;
let val = (((b0 & 0x07) as u64) << 32) | (b[0] as u64) << 24 | (b[1] as u64) << 16 | (b[2] as u64) << 8 | b[3] as u64;
let val = (((b0 & 0x07) as u64) << 32)
| (b[0] as u64) << 24
| (b[1] as u64) << 16
| (b[2] as u64) << 8
| b[3] as u64;
Ok((val, 5))
} else if b0 & 0x04 != 0 {
let mut b = [0u8; 5];
r.read_exact(&mut b)?;
let val = (((b0 & 0x03) as u64) << 40) | (b[0] as u64) << 32 | (b[1] as u64) << 24 | (b[2] as u64) << 16 | (b[3] as u64) << 8 | b[4] as u64;
let val = (((b0 & 0x03) as u64) << 40)
| (b[0] as u64) << 32
| (b[1] as u64) << 24
| (b[2] as u64) << 16
| (b[3] as u64) << 8
| b[4] as u64;
Ok((val, 6))
} else if b0 & 0x02 != 0 {
let mut b = [0u8; 6];
r.read_exact(&mut b)?;
let val = (((b0 & 0x01) as u64) << 48) | (b[0] as u64) << 40 | (b[1] as u64) << 32 | (b[2] as u64) << 24 | (b[3] as u64) << 16 | (b[4] as u64) << 8 | b[5] as u64;
let val = (((b0 & 0x01) as u64) << 48)
| (b[0] as u64) << 40
| (b[1] as u64) << 32
| (b[2] as u64) << 24
| (b[3] as u64) << 16
| (b[4] as u64) << 8
| b[5] as u64;
Ok((val, 7))
} else {
let mut b = [0u8; 7];
r.read_exact(&mut b)?;
let val = (b[0] as u64) << 48 | (b[1] as u64) << 40 | (b[2] as u64) << 32 | (b[3] as u64) << 24 | (b[4] as u64) << 16 | (b[5] as u64) << 8 | b[6] as u64;
if val == 0x00FFFFFFFFFFFFFF { return Ok((u64::MAX, 8)); }
let val = (b[0] as u64) << 48
| (b[1] as u64) << 40
| (b[2] as u64) << 32
| (b[3] as u64) << 24
| (b[4] as u64) << 16
| (b[5] as u64) << 8
| b[6] as u64;
if val == 0x00FFFFFFFFFFFFFF {
return Ok((u64::MAX, 8));
}
Ok((val, 8))
}
}
@@ -258,7 +299,9 @@ pub fn read_string_val(r: &mut impl Read, len: usize) -> io::Result<String> {
let mut buf = vec![0u8; len];
r.read_exact(&mut buf)?;
// Strip trailing nulls
while buf.last() == Some(&0) { buf.pop(); }
while buf.last() == Some(&0) {
buf.pop();
}
String::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
@@ -274,13 +317,18 @@ pub fn read_vint(r: &mut impl Read) -> io::Result<(u64, usize)> {
let mut first = [0u8; 1];
r.read_exact(&mut first)?;
let b0 = first[0];
if b0 & 0x80 != 0 { return Ok(((b0 & 0x7F) as u64, 1)); }
if b0 & 0x80 != 0 {
return Ok(((b0 & 0x7F) as u64, 1));
}
if b0 & 0x40 != 0 {
let mut b = [0u8; 1];
r.read_exact(&mut b)?;
return Ok(((((b0 & 0x3F) as u64) << 8) | b[0] as u64, 2));
}
Err(io::Error::new(io::ErrorKind::InvalidData, "unsupported VINT width"))
Err(io::Error::new(
io::ErrorKind::InvalidData,
"unsupported VINT width",
))
}
// ============================================================
@@ -389,7 +437,7 @@ mod tests {
fn test_write_uint() {
let mut buf = Vec::new();
write_uint(&mut buf, 0x4286, 1).unwrap(); // EBML_VERSION = 1
// ID: 42 86, Size: 81 (1 byte), Data: 01
// ID: 42 86, Size: 81 (1 byte), Data: 01
assert_eq!(buf, [0x42, 0x86, 0x81, 0x01]);
}
@@ -460,7 +508,19 @@ mod tests {
#[test]
fn write_read_size_roundtrip() {
let test_sizes: &[u64] = &[0, 1, 0x7E, 127, 128, 0x3FFE, 16383, 16384, 0x1FFFFE, 0x0FFFFFFE, 0x1_0000_0000];
let test_sizes: &[u64] = &[
0,
1,
0x7E,
127,
128,
0x3FFE,
16383,
16384,
0x1FFFFE,
0x0FFFFFFE,
0x1_0000_0000,
];
for &size in test_sizes {
let mut buf = Vec::new();
write_size(&mut buf, size).unwrap();
@@ -472,7 +532,17 @@ mod tests {
#[test]
fn write_read_uint_roundtrip() {
let test_vals: &[u64] = &[0, 1, 127, 255, 256, 0xFFFF, 0xFF_FFFF, 0xFFFF_FFFF, 1_000_000_000_000];
let test_vals: &[u64] = &[
0,
1,
127,
255,
256,
0xFFFF,
0xFF_FFFF,
0xFFFF_FFFF,
1_000_000_000_000,
];
let test_id = EBML_VERSION;
for &val in test_vals {
let mut buf = Vec::new();
@@ -488,7 +558,13 @@ mod tests {
#[test]
fn write_read_string_roundtrip() {
let test_strings = &["", "matroska", "freemkv", "Hello, World!", "unicode: \u{1F600}"];
let test_strings = &[
"",
"matroska",
"freemkv",
"Hello, World!",
"unicode: \u{1F600}",
];
let test_id = EBML_DOC_TYPE;
for &s in test_strings {
let mut buf = Vec::new();
@@ -504,7 +580,16 @@ mod tests {
#[test]
fn write_read_float_roundtrip() {
let test_vals: &[f64] = &[0.0, 1.0, -1.0, 3.14159265358979, 48000.0, 7200000.0, f64::MIN, f64::MAX];
let test_vals: &[f64] = &[
0.0,
1.0,
-1.0,
3.14159265358979,
48000.0,
7200000.0,
f64::MIN,
f64::MAX,
];
let test_id = DURATION;
for &val in test_vals {
let mut buf = Vec::new();
@@ -515,7 +600,12 @@ mod tests {
let (size, _) = read_size(&mut cursor).unwrap();
assert_eq!(size, 8);
let read_val = read_float_val(&mut cursor, size as usize).unwrap();
assert_eq!(read_val.to_bits(), val.to_bits(), "float roundtrip failed for {}", val);
assert_eq!(
read_val.to_bits(),
val.to_bits(),
"float roundtrip failed for {}",
val
);
}
}
@@ -526,7 +616,10 @@ mod tests {
assert_eq!(buf.len(), 8);
assert_eq!(buf[0], 0x01);
for &b in &buf[1..] {
assert_eq!(b, 0xFF, "unknown size bytes should all be 0xFF after first byte");
assert_eq!(
b, 0xFF,
"unknown size bytes should all be 0xFF after first byte"
);
}
// Reading it back should yield u64::MAX
let mut cursor = Cursor::new(&buf);
+146 -97
View File
@@ -4,15 +4,17 @@
//! DiscStream (titles, streams, labels, AACS). An ISO is a flat image of
//! 2048-byte sectors — sector N starts at byte offset N * 2048.
//!
//! Write: creates a sector-by-sector disc image from a SectorReader source.
//! Write: creates a UDF 2.50 filesystem containing the m2ts stream data.
//! The resulting ISO can be mounted or read back via IsoStream.
use std::io::{self, Read, Write, Seek, SeekFrom};
use std::fs::File;
use std::path::Path;
use super::isowriter::IsoWriter;
use super::IOStream;
use crate::disc::{Disc, DiscTitle, ScanOptions};
use crate::sector::SectorReader;
use crate::error::{Error, Result};
use crate::sector::SectorReader;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
const SECTOR_SIZE: u64 = 2048;
@@ -31,15 +33,19 @@ impl IsoSectorReader {
Ok(Self { file, capacity })
}
pub fn capacity(&self) -> u32 { self.capacity }
pub fn capacity(&self) -> u32 {
self.capacity
}
}
impl SectorReader for IsoSectorReader {
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let bytes = count as usize * SECTOR_SIZE as usize;
self.file.seek(SeekFrom::Start(lba as u64 * SECTOR_SIZE))
self.file
.seek(SeekFrom::Start(lba as u64 * SECTOR_SIZE))
.map_err(|e| Error::IoError { source: e })?;
self.file.read_exact(&mut buf[..bytes])
self.file
.read_exact(&mut buf[..bytes])
.map_err(|e| Error::IoError { source: e })?;
Ok(bytes)
}
@@ -48,13 +54,12 @@ impl SectorReader for IsoSectorReader {
/// Blu-ray ISO image stream.
///
/// Read: opens ISO, parses UDF (same as DiscStream), streams BD-TS content.
/// Write: receives sector data and writes to ISO file.
/// Write: creates UDF 2.50 ISO with BDMV/STREAM/*.m2ts.
pub struct IsoStream {
disc_title: DiscTitle,
disc: Option<Disc>,
// Read side
reader: Option<IsoSectorReader>,
writer: Option<io::BufWriter<File>>,
/// Sector ranges to read: (start_lba, sector_count)
extents: Vec<(u32, u32)>,
extent_idx: usize,
sectors_remaining: u32,
@@ -62,6 +67,9 @@ pub struct IsoStream {
buf_pos: usize,
buf_len: usize,
eof: bool,
// Write side
iso_writer: Option<IsoWriter<io::BufWriter<File>>>,
write_started: bool,
}
impl IsoStream {
@@ -71,26 +79,31 @@ impl IsoStream {
let capacity = reader.capacity();
let disc = Disc::scan_image(&mut reader, capacity, opts)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
.map_err(|e| io::Error::other(e.to_string()))?;
let idx = title_index.unwrap_or(0).min(disc.titles.len().saturating_sub(1));
let idx = title_index
.unwrap_or(0)
.min(disc.titles.len().saturating_sub(1));
let disc_title = if disc.titles.is_empty() {
return Err(io::Error::new(io::ErrorKind::NotFound, "no titles found in ISO image"));
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no titles found in ISO image",
));
} else {
disc.titles[idx].clone()
};
let extents: Vec<(u32, u32)> = disc_title.extents.iter()
let extents: Vec<(u32, u32)> = disc_title
.extents
.iter()
.map(|e| (e.start_lba, e.sector_count))
.collect();
let sectors_remaining = extents.first().map(|e| e.1).unwrap_or(0);
Ok(IsoStream {
disc_title,
disc: Some(disc),
reader: Some(reader),
writer: None,
extents,
extent_idx: 0,
sectors_remaining,
@@ -98,20 +111,22 @@ impl IsoStream {
buf_pos: 0,
buf_len: 0,
eof: false,
iso_writer: None,
write_started: false,
})
}
/// Create an ISO file for writing. Receives raw sector data.
/// Create an ISO file for writing.
pub fn create(path: &str) -> io::Result<Self> {
let file = File::create(Path::new(path))
.map_err(|e| io::Error::new(e.kind(), format!("iso://{}: {}", path, e)))?;
let writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
let buf_writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
let iso_writer = IsoWriter::new(buf_writer, "FREEMKV", "00001.m2ts");
Ok(IsoStream {
disc_title: DiscTitle::empty(),
disc: None,
reader: None,
writer: Some(writer),
extents: Vec::new(),
extent_idx: 0,
sectors_remaining: 0,
@@ -119,19 +134,35 @@ impl IsoStream {
buf_pos: 0,
buf_len: 0,
eof: false,
iso_writer: Some(iso_writer),
write_started: false,
})
}
/// Set metadata (for write mode).
/// Set metadata (for write mode). Must be called before writing data.
pub fn meta(mut self, dt: &DiscTitle) -> Self {
self.disc_title = dt.clone();
// Update the ISO writer's volume ID and m2ts filename from title metadata
if let Some(writer) = self.iso_writer.take() {
let vol_id = if dt.playlist.is_empty() {
"FREEMKV".to_string()
} else {
dt.playlist
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == ' ')
.collect::<String>()
};
let m2ts_name = format!("{:05}.m2ts", dt.playlist_id.max(1));
self.iso_writer = Some(writer.with_names(&vol_id, &m2ts_name));
}
self
}
/// Get the full Disc (for listing all titles).
pub fn disc(&self) -> Option<&Disc> { self.disc.as_ref() }
pub fn disc(&self) -> Option<&Disc> {
self.disc.as_ref()
}
/// Read the next sector from the current extent.
fn read_next_sector(&mut self) -> io::Result<bool> {
let reader = match self.reader.as_mut() {
Some(r) => r,
@@ -146,8 +177,9 @@ impl IsoStream {
let offset = total - self.sectors_remaining;
let lba = start_lba + offset;
reader.read_sectors(lba, 1, &mut self.sector_buf)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
reader
.read_sectors(lba, 1, &mut self.sector_buf)
.map_err(|e| io::Error::other(e.to_string()))?;
self.buf_pos = 0;
self.buf_len = SECTOR_SIZE as usize;
@@ -164,10 +196,12 @@ impl IsoStream {
}
impl IOStream for IsoStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if let Some(ref mut w) = self.writer {
w.flush()?;
if let Some(ref mut w) = self.iso_writer {
w.finish()?;
}
Ok(())
}
@@ -175,9 +209,10 @@ impl IOStream for IsoStream {
impl Read for IsoStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.eof { return Ok(0); }
if self.eof {
return Ok(0);
}
// Drain current sector buffer
if self.buf_pos < self.buf_len {
let n = (self.buf_len - self.buf_pos).min(buf.len());
buf[..n].copy_from_slice(&self.sector_buf[self.buf_pos..self.buf_pos + n]);
@@ -185,7 +220,6 @@ impl Read for IsoStream {
return Ok(n);
}
// Read next sector
if self.read_next_sector()? {
let n = self.buf_len.min(buf.len());
buf[..n].copy_from_slice(&self.sector_buf[..n]);
@@ -198,93 +232,108 @@ impl Read for IsoStream {
}
}
impl Write for IsoStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let w = match self.iso_writer.as_mut() {
Some(w) => w,
None => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"iso:// opened for reading — cannot write",
))
}
};
if !self.write_started {
w.start()?;
self.write_started = true;
}
w.write_data(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use crate::sector::SectorReader;
#[test]
fn iso_reader_read_sectors() {
// Create a temp file with known sector data
let dir = std::env::temp_dir();
let path = dir.join("libfreemkv_test_iso_sectors.iso");
let path_str = path.to_str().unwrap();
// Write 4 sectors of known data
{
let mut f = File::create(&path).unwrap();
for sector_idx in 0u8..4 {
let mut sector = [sector_idx; SECTOR_SIZE as usize];
sector[0] = sector_idx;
sector[2047] = sector_idx.wrapping_mul(0x37);
f.write_all(&sector).unwrap();
}
f.flush().unwrap();
let mut data = vec![0u8; 4 * SECTOR_SIZE as usize];
for i in 0..4u8 {
let offset = i as usize * SECTOR_SIZE as usize;
data[offset] = i + 1;
data[offset + 2047] = i + 100;
}
let mut reader = IsoSectorReader::open(path_str).unwrap();
let dir = std::env::temp_dir().join("freemkv_test_iso_read");
std::fs::write(&dir, &data).unwrap();
let mut reader = IsoSectorReader::open(dir.to_str().unwrap()).unwrap();
assert_eq!(reader.capacity(), 4);
// Read sector 0
let mut buf = [0u8; SECTOR_SIZE as usize];
let n = reader.read_sectors(0, 1, &mut buf).unwrap();
assert_eq!(n, SECTOR_SIZE as usize);
assert_eq!(buf[0], 0);
assert_eq!(buf[2047], 0u8.wrapping_mul(0x37));
let mut buf = [0u8; 2048];
reader.read_sectors(0, 1, &mut buf).unwrap();
assert_eq!(buf[0], 1);
assert_eq!(buf[2047], 100);
// Read sector 2
let n = reader.read_sectors(2, 1, &mut buf).unwrap();
assert_eq!(n, SECTOR_SIZE as usize);
assert_eq!(buf[0], 2);
assert_eq!(buf[1], 2); // filled with sector_idx
assert_eq!(buf[2047], 2u8.wrapping_mul(0x37));
reader.read_sectors(2, 1, &mut buf).unwrap();
assert_eq!(buf[0], 3);
assert_eq!(buf[2047], 102);
// Read 2 sectors at once (sectors 1 and 2)
let mut buf2 = [0u8; SECTOR_SIZE as usize * 2];
let n = reader.read_sectors(1, 2, &mut buf2).unwrap();
assert_eq!(n, SECTOR_SIZE as usize * 2);
assert_eq!(buf2[0], 1); // sector 1 first byte
assert_eq!(buf2[SECTOR_SIZE as usize], 2); // sector 2 first byte
// Clean up
let _ = std::fs::remove_file(&path);
std::fs::remove_file(&dir).ok();
}
#[test]
fn iso_reader_capacity() {
let dir = std::env::temp_dir();
let path = dir.join("libfreemkv_test_iso_capacity.iso");
let path_str = path.to_str().unwrap();
let data = vec![0u8; 10 * SECTOR_SIZE as usize];
let dir = std::env::temp_dir().join("freemkv_test_iso_cap");
std::fs::write(&dir, &data).unwrap();
// Write exactly 10 sectors
{
let mut f = File::create(&path).unwrap();
let data = vec![0u8; SECTOR_SIZE as usize * 10];
f.write_all(&data).unwrap();
f.flush().unwrap();
}
let reader = IsoSectorReader::open(path_str).unwrap();
let reader = IsoSectorReader::open(dir.to_str().unwrap()).unwrap();
assert_eq!(reader.capacity(), 10);
// Clean up
let _ = std::fs::remove_file(&path);
std::fs::remove_file(&dir).ok();
}
}
impl Write for IsoStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.writer.as_mut() {
Some(w) => w.write(buf),
None => Err(io::Error::new(io::ErrorKind::Unsupported,
"iso:// opened for reading — cannot write")),
}
}
fn flush(&mut self) -> io::Result<()> {
match self.writer.as_mut() {
Some(w) => w.flush(),
None => Ok(()),
#[test]
fn iso_write_creates_valid_udf() {
let path = std::env::temp_dir().join("freemkv_test_iso_write.iso");
let mut stream = IsoStream::create(path.to_str().unwrap()).unwrap();
// Write some fake BD-TS content
let mut content = Vec::new();
for i in 0..100u8 {
let mut pkt = [0u8; 192];
pkt[4] = 0x47;
pkt[5] = i;
content.extend_from_slice(&pkt);
}
stream.write_all(&content).unwrap();
stream.finish().unwrap();
// Verify the ISO has valid UDF structure
let file = File::open(&path).unwrap();
let size = file.metadata().unwrap().len();
assert!(size > 288 * SECTOR_SIZE); // at least header + some data
// Read back and verify AVDP at sector 256
let mut reader = IsoSectorReader::open(path.to_str().unwrap()).unwrap();
let mut avdp = [0u8; 2048];
reader.read_sectors(256, 1, &mut avdp).unwrap();
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
assert_eq!(tag_id, 2, "AVDP tag should be 2");
// Verify VRS at sector 16
let mut vrs = [0u8; 2048];
reader.read_sectors(16, 1, &mut vrs).unwrap();
assert_eq!(&vrs[1..6], b"BEA01");
std::fs::remove_file(&path).ok();
}
}
+471
View File
@@ -0,0 +1,471 @@
//! UDF ISO writer — creates Blu-ray disc images.
//!
//! Writes a minimal UDF 2.50 filesystem containing BDMV/STREAM/*.m2ts.
//! The ISO can be mounted or read back via IsoStream.
//!
//! Layout:
//! Sector 0-15: System area (zeros)
//! Sector 16-18: Volume Recognition Sequence (BEA01, NSR03, TEA01)
//! Sector 32-37: Volume Descriptor Sequence
//! Sector 256: Anchor Volume Descriptor Pointer
//! Sector 260-271: Metadata partition (FSD, ICBs, directories)
//! Sector 288+: File data (m2ts content)
//! Last-256: Reserve AVDP
use std::io::{self, Seek, SeekFrom, Write};
const SECTOR_SIZE: u64 = 2048;
// Layout constants
const VRS_START: u32 = 16; // Volume Recognition Sequence
const VDS_START: u32 = 32; // Volume Descriptor Sequence
const AVDP_SECTOR: u32 = 256; // Anchor Volume Descriptor Pointer
const PARTITION_START: u32 = 257; // Physical partition start
const METADATA_START: u32 = 260; // Metadata partition content
const FSD_SECTOR: u32 = 260; // File Set Descriptor
const ROOT_ICB_SECTOR: u32 = 261; // Root directory ICB
const ROOT_DIR_SECTOR: u32 = 262; // Root directory data
const BDMV_ICB_SECTOR: u32 = 263; // BDMV/ ICB
const BDMV_DIR_SECTOR: u32 = 264; // BDMV/ directory data
const STREAM_ICB_SECTOR: u32 = 265; // BDMV/STREAM/ ICB
const STREAM_DIR_SECTOR: u32 = 266; // BDMV/STREAM/ directory data
const M2TS_ICB_SECTOR: u32 = 267; // m2ts file ICB
const DATA_START: u32 = 288; // Start of file data (aligned)
/// Write a complete BD ISO image.
///
/// Writes UDF structure, then streams m2ts content from the writer.
/// Call `start()` first, then write BD-TS bytes, then call `finish()`.
pub struct IsoWriter<W: Write + Seek> {
writer: W,
volume_id: String,
m2ts_name: String,
data_start_sector: u32,
bytes_written: u64,
}
impl<W: Write + Seek> IsoWriter<W> {
/// Create a new ISO writer. Call `start()` to write the UDF header.
pub fn new(writer: W, volume_id: &str, m2ts_name: &str) -> Self {
Self {
writer,
volume_id: volume_id.to_string(),
m2ts_name: m2ts_name.to_string(),
data_start_sector: DATA_START,
bytes_written: 0,
}
}
/// Update volume ID and m2ts filename. Must be called before `start()`.
pub fn with_names(mut self, volume_id: &str, m2ts_name: &str) -> Self {
self.volume_id = volume_id.to_string();
self.m2ts_name = m2ts_name.to_string();
self
}
/// Write UDF filesystem header. After this, write m2ts content bytes.
pub fn start(&mut self) -> io::Result<()> {
// System area: sectors 0-15 (zeros)
let zero_sector = [0u8; SECTOR_SIZE as usize];
for _ in 0..VRS_START {
self.writer.write_all(&zero_sector)?;
}
// Volume Recognition Sequence
self.write_vrs()?;
// Pad sectors 19-31
for _ in 19..VDS_START {
self.writer.write_all(&zero_sector)?;
}
// Volume Descriptor Sequence (sectors 32-37)
self.write_vds()?;
// Pad sectors 38-255
for _ in 38..AVDP_SECTOR {
self.writer.write_all(&zero_sector)?;
}
// AVDP at sector 256
self.write_avdp()?;
// Partition area: metadata file ICB at partition_start
self.write_metadata_file_icb()?;
// Pad to metadata start
for _ in (PARTITION_START + 1)..METADATA_START {
self.writer.write_all(&zero_sector)?;
}
// Metadata partition
self.write_fsd()?;
self.write_root_icb()?;
self.write_root_dir()?;
self.write_bdmv_icb()?;
self.write_bdmv_dir()?;
self.write_stream_icb()?;
self.write_stream_dir()?;
self.write_m2ts_icb(0)?; // placeholder size, updated in finish()
// Pad to data start
for _ in (M2TS_ICB_SECTOR + 1)..self.data_start_sector {
self.writer.write_all(&zero_sector)?;
}
Ok(())
}
/// Write m2ts content bytes. Call after `start()`.
pub fn write_data(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.writer.write(buf)?;
self.bytes_written += n as u64;
Ok(n)
}
/// Finalize the ISO: pad to sector boundary, update file sizes, write reserve AVDP.
pub fn finish(&mut self) -> io::Result<()> {
// Pad to sector boundary
let remainder = (self.bytes_written % SECTOR_SIZE) as usize;
if remainder > 0 {
let pad = SECTOR_SIZE as usize - remainder;
let zeros = vec![0u8; pad];
self.writer.write_all(&zeros)?;
self.bytes_written += pad as u64;
}
let total_data_sectors = (self.bytes_written / SECTOR_SIZE) as u32;
let total_sectors = self.data_start_sector + total_data_sectors;
// Seek back and update m2ts file ICB with actual size
self.writer
.seek(SeekFrom::Start(M2TS_ICB_SECTOR as u64 * SECTOR_SIZE))?;
self.write_m2ts_icb(self.bytes_written)?;
// Seek to end and write reserve AVDP
let reserve_sector = total_sectors;
self.writer
.seek(SeekFrom::Start(reserve_sector as u64 * SECTOR_SIZE))?;
self.write_avdp()?;
self.writer.flush()?;
Ok(())
}
// ── UDF structure writers ──────────────────────────────────────────────
fn write_vrs(&mut self) -> io::Result<()> {
// BEA01 at sector 16
let mut bea = [0u8; SECTOR_SIZE as usize];
bea[0] = 0; // structure type
bea[1..6].copy_from_slice(b"BEA01");
bea[6] = 1; // structure version
self.writer.write_all(&bea)?;
// NSR03 at sector 17 (UDF 2.50)
let mut nsr = [0u8; SECTOR_SIZE as usize];
nsr[0] = 0;
nsr[1..6].copy_from_slice(b"NSR03");
nsr[6] = 1;
self.writer.write_all(&nsr)?;
// TEA01 at sector 18
let mut tea = [0u8; SECTOR_SIZE as usize];
tea[0] = 0;
tea[1..6].copy_from_slice(b"TEA01");
tea[6] = 1;
self.writer.write_all(&tea)?;
Ok(())
}
fn write_vds(&mut self) -> io::Result<()> {
// Primary Volume Descriptor (tag 1) at sector 32
let mut pvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut pvd, 1, VDS_START);
// Volume Identifier at offset 24 (32-byte d-string)
write_dstring(&mut pvd[24..56], &self.volume_id);
self.writer.write_all(&pvd)?;
// Partition Descriptor (tag 5) at sector 33
let mut pd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut pd, 5, VDS_START + 1);
// Partition starting location at offset 188
pd[188..192].copy_from_slice(&PARTITION_START.to_le_bytes());
// Partition length (large enough for everything)
let part_len: u32 = 0xFFFFFFFF;
pd[192..196].copy_from_slice(&part_len.to_le_bytes());
self.writer.write_all(&pd)?;
// Logical Volume Descriptor (tag 6) at sector 34
let mut lvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut lvd, 6, VDS_START + 2);
// Logical block size at offset 212
lvd[212..216].copy_from_slice(&2048u32.to_le_bytes());
// Number of partition maps at offset 268
lvd[268..272].copy_from_slice(&2u32.to_le_bytes());
// Partition map 1: Type 1 (physical), 6 bytes
lvd[440] = 1; // type
lvd[441] = 6; // length
// Partition map 2: Type 2 (metadata), 64 bytes
lvd[446] = 2; // type
lvd[447] = 64; // length
// Entity ID for metadata partition
lvd[450..473].copy_from_slice(b"*UDF Metadata Partition");
self.writer.write_all(&lvd)?;
// Unallocated Space Descriptor (tag 7) at sector 35
let mut usd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut usd, 7, VDS_START + 3);
self.writer.write_all(&usd)?;
// Implementation Use Volume Descriptor (tag 4) at sector 36
let mut iuvd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut iuvd, 4, VDS_START + 4);
self.writer.write_all(&iuvd)?;
// Terminating Descriptor (tag 8) at sector 37
let mut td = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut td, 8, VDS_START + 5);
self.writer.write_all(&td)?;
Ok(())
}
fn write_avdp(&mut self) -> io::Result<()> {
let mut avdp = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut avdp, 2, AVDP_SECTOR);
// Main VDS extent_ad: {length, location} per UDF spec
avdp[16..20].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
avdp[20..24].copy_from_slice(&VDS_START.to_le_bytes()); // location
// Reserve VDS extent_ad (same as main for simplicity)
avdp[24..28].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
avdp[28..32].copy_from_slice(&VDS_START.to_le_bytes()); // location
self.writer.write_all(&avdp)?;
Ok(())
}
fn write_metadata_file_icb(&mut self) -> io::Result<()> {
// Extended File Entry (tag 266) at partition_start
// Points to metadata content at METADATA_START
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, PARTITION_START);
// ICB tag at offset 16
icb[16..20].copy_from_slice(&0u32.to_le_bytes()); // prior recorded
icb[20..22].copy_from_slice(&0u16.to_le_bytes()); // strategy type
icb[22..24].copy_from_slice(&0u16.to_le_bytes()); // strategy parameter
// File type at offset 27: 250 = metadata file
icb[27] = 250;
// Information length at offset 56
let meta_len: u64 = 12 * SECTOR_SIZE; // 12 sectors of metadata
icb[56..64].copy_from_slice(&meta_len.to_le_bytes());
// Extended attribute length at offset 208
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
// Allocation descriptor at offset 216: short_ad (length + position)
let ad_len = meta_len as u32;
let ad_pos = METADATA_START - PARTITION_START; // relative to partition
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_fsd(&mut self) -> io::Result<()> {
let mut fsd = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut fsd, 256, FSD_SECTOR);
// Root Directory ICB (long_ad at offset 400)
let root_lba = ROOT_ICB_SECTOR - METADATA_START; // metadata-relative
fsd[400..404].copy_from_slice(&SECTOR_SIZE.to_le_bytes()[..4]); // extent length
fsd[404..408].copy_from_slice(&root_lba.to_le_bytes());
self.writer.write_all(&fsd)?;
Ok(())
}
fn write_root_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, ROOT_ICB_SECTOR);
icb[27] = 4; // file type: directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = ROOT_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_root_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
// Parent entry (.. points to self)
offset += write_fid(
&mut dir[offset..],
ROOT_ICB_SECTOR - METADATA_START,
"",
true,
);
// BDMV directory entry
offset += write_fid(
&mut dir[offset..],
BDMV_ICB_SECTOR - METADATA_START,
"BDMV",
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_bdmv_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, BDMV_ICB_SECTOR);
icb[27] = 4; // directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = BDMV_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_bdmv_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
offset += write_fid(
&mut dir[offset..],
ROOT_ICB_SECTOR - METADATA_START,
"",
true,
);
offset += write_fid(
&mut dir[offset..],
STREAM_ICB_SECTOR - METADATA_START,
"STREAM",
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_stream_icb(&mut self) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, STREAM_ICB_SECTOR);
icb[27] = 4; // directory
let dir_len: u64 = SECTOR_SIZE;
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
let ad_pos = STREAM_DIR_SECTOR - METADATA_START;
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
fn write_stream_dir(&mut self) -> io::Result<()> {
let mut dir = [0u8; SECTOR_SIZE as usize];
let mut offset = 0;
offset += write_fid(
&mut dir[offset..],
BDMV_ICB_SECTOR - METADATA_START,
"",
true,
);
offset += write_fid(
&mut dir[offset..],
M2TS_ICB_SECTOR - METADATA_START,
&self.m2ts_name,
false,
);
let _ = offset;
self.writer.write_all(&dir)?;
Ok(())
}
fn write_m2ts_icb(&mut self, file_size: u64) -> io::Result<()> {
let mut icb = [0u8; SECTOR_SIZE as usize];
write_descriptor_tag(&mut icb, 266, M2TS_ICB_SECTOR);
icb[27] = 5; // file type: regular file
icb[56..64].copy_from_slice(&file_size.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
// Allocation: data starts at DATA_START in the physical partition
let data_offset = self.data_start_sector - PARTITION_START;
// Cap allocation length at u32::MAX for files >4GB (UDF short_ad limitation)
let ad_len = if file_size > u32::MAX as u64 { u32::MAX } else { file_size as u32 };
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
icb[220..224].copy_from_slice(&data_offset.to_le_bytes());
self.writer.write_all(&icb)?;
Ok(())
}
}
// ── UDF primitives ─────────────────────────────────────────────────────────
/// Write a UDF Descriptor Tag at the start of a sector.
fn write_descriptor_tag(buf: &mut [u8], tag_id: u16, sector: u32) {
buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
// Descriptor version: 3 (UDF 2.50)
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
// Tag serial number
buf[4] = 0;
// Descriptor CRC (simplified — set to 0, most implementations accept this)
buf[8..10].copy_from_slice(&0u16.to_le_bytes());
// Descriptor CRC length
buf[10..12].copy_from_slice(&0u16.to_le_bytes());
// Tag location
buf[12..16].copy_from_slice(&sector.to_le_bytes());
}
/// Write a UDF d-string (compressed unicode string with length prefix).
fn write_dstring(buf: &mut [u8], s: &str) {
let max = buf.len() - 1; // last byte is length
let bytes = s.as_bytes();
let len = bytes.len().min(max);
if len > 0 {
buf[0] = 8; // compression ID: 8 = Latin-1
buf[1..1 + len].copy_from_slice(&bytes[..len]);
buf[buf.len() - 1] = (len + 1) as u8; // d-string length including comp ID
}
}
/// Write a File Identifier Descriptor. Returns bytes written (4-byte aligned).
fn write_fid(buf: &mut [u8], icb_lba: u32, name: &str, is_parent: bool) -> usize {
// Tag 257 = File Identifier Descriptor
let name_bytes = name.as_bytes();
let name_len = if is_parent { 0 } else { name_bytes.len() + 1 }; // +1 for comp ID
let fid_len = 38 + name_len; // fixed header + identifier
let padded = (fid_len + 3) & !3; // 4-byte align
if padded > buf.len() {
return 0;
}
// Tag
buf[0..2].copy_from_slice(&257u16.to_le_bytes());
// File version number at offset 16
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
// File characteristics at offset 18
buf[18] = if is_parent { 0x0A } else { 0x02 }; // parent | directory
if !is_parent && !name.contains('.') {
buf[18] = 0x02; // directory
} else if !is_parent {
buf[18] = 0x00; // file
}
// ICB (long_ad at offset 20): extent length + location
buf[20..24].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
buf[24..28].copy_from_slice(&icb_lba.to_le_bytes());
// Identifier length at offset 36
buf[36] = name_len as u8;
// Implementation use length at offset 37
buf[37] = 0;
// File identifier at offset 38
if !is_parent && !name_bytes.is_empty() {
buf[38] = 8; // compression ID: Latin-1
buf[39..39 + name_bytes.len()].copy_from_slice(name_bytes);
}
padded
}
+26 -9
View File
@@ -3,9 +3,9 @@
//! Write: prepends FMKV metadata header, then passes through BD-TS bytes.
//! Read: extracts metadata header (or scans PMT), then yields BD-TS bytes.
use std::io::{self, Read, Write, Seek, SeekFrom};
use super::{IOStream, ReadSeek, meta, ts};
use super::{meta, ts, IOStream, ReadSeek};
use crate::disc::{DiscTitle, Stream as DiscStream};
use std::io::{self, Read, Seek, SeekFrom, Write};
/// Size of initial scan buffer for PMT/stream detection.
const SCAN_SIZE: usize = 1024 * 1024;
@@ -54,7 +54,9 @@ impl M2tsStream {
if let Ok(Some(m)) = meta::read_header(&mut reader) {
return Ok(Self {
disc_title: m.to_title(),
mode: Mode::Read { reader: Box::new(reader) },
mode: Mode::Read {
reader: Box::new(reader),
},
finished: false,
});
}
@@ -83,17 +85,23 @@ impl M2tsStream {
streams,
..DiscTitle::empty()
},
mode: Mode::Read { reader: Box::new(reader) },
mode: Mode::Read {
reader: Box::new(reader),
},
finished: false,
})
}
}
impl IOStream for M2tsStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if self.finished { return Ok(()); }
if self.finished {
return Ok(());
}
self.finished = true;
if let Mode::Write { ref mut writer, .. } = self.mode {
writer.flush()
@@ -106,7 +114,10 @@ impl IOStream for M2tsStream {
impl Write for M2tsStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.mode {
Mode::Write { ref mut writer, ref mut header_written } => {
Mode::Write {
ref mut writer,
ref mut header_written,
} => {
if !*header_written {
if !self.disc_title.streams.is_empty() {
let m = meta::M2tsMeta::from_title(&self.disc_title);
@@ -116,7 +127,10 @@ impl Write for M2tsStream {
}
writer.write(buf)
}
Mode::Read { .. } => Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for reading")),
Mode::Read { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for reading",
)),
}
}
@@ -133,7 +147,10 @@ impl Read for M2tsStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.mode {
Mode::Read { ref mut reader } => reader.read(buf),
Mode::Write { .. } => Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for writing")),
Mode::Write { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for writing",
)),
}
}
}
+99 -62
View File
@@ -3,10 +3,11 @@
//! 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 serde::{Deserialize, Serialize};
use std::io::{self, Read, Seek, SeekFrom, Write};
use serde::{Serialize, Deserialize};
use crate::disc::{DiscTitle, Stream, VideoStream, AudioStream, SubtitleStream,
Codec, HdrFormat, ColorSpace};
/// Magic bytes: "FMKV" + version 1 + 2 reserved bytes.
const MAGIC: [u8; 8] = [b'F', b'M', b'K', b'V', 0x00, 0x01, 0x00, 0x00];
@@ -37,60 +38,76 @@ pub enum MetaStream {
Video {
pid: u16,
codec: String,
#[serde(default)] resolution: String,
#[serde(default)] frame_rate: String,
#[serde(default)] hdr: String,
#[serde(default)] label: String,
#[serde(default)] secondary: bool,
#[serde(default)]
resolution: String,
#[serde(default)]
frame_rate: String,
#[serde(default)]
hdr: String,
#[serde(default)]
label: String,
#[serde(default)]
secondary: bool,
},
#[serde(rename = "audio")]
Audio {
pid: u16,
codec: String,
#[serde(default)] channels: String,
#[serde(default)] language: String,
#[serde(default)] sample_rate: String,
#[serde(default)] label: String,
#[serde(default)] secondary: bool,
#[serde(default)]
channels: String,
#[serde(default)]
language: String,
#[serde(default)]
sample_rate: String,
#[serde(default)]
label: String,
#[serde(default)]
secondary: bool,
},
#[serde(rename = "subtitle")]
Subtitle {
pid: u16,
codec: String,
#[serde(default)] language: String,
#[serde(default)] forced: bool,
#[serde(default)]
language: String,
#[serde(default)]
forced: bool,
},
}
impl M2tsMeta {
/// Build metadata from a disc Title.
pub fn from_title(title: &DiscTitle) -> Self {
let streams = title.streams.iter().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),
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(),
language: a.language.clone(),
sample_rate: a.sample_rate.clone(),
label: a.label.clone(),
secondary: a.secondary,
},
Stream::Subtitle(s) => MetaStream::Subtitle {
pid: s.pid,
codec: codec_to_str(s.codec),
language: s.language.clone(),
forced: s.forced,
},
}).collect();
let streams = title
.streams
.iter()
.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),
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(),
language: a.language.clone(),
sample_rate: a.sample_rate.clone(),
label: a.label.clone(),
secondary: a.secondary,
},
Stream::Subtitle(s) => MetaStream::Subtitle {
pid: s.pid,
codec: codec_to_str(s.codec),
language: s.language.clone(),
forced: s.forced,
},
})
.collect();
Self {
v: 1,
@@ -102,9 +119,19 @@ impl M2tsMeta {
/// Convert back to a library Title (for remux).
pub fn to_title(&self) -> DiscTitle {
let streams = self.streams.iter().map(|s| match s {
MetaStream::Video { pid, codec, resolution, frame_rate, hdr, label, secondary } => {
Stream::Video(VideoStream {
let streams = self
.streams
.iter()
.map(|s| match s {
MetaStream::Video {
pid,
codec,
resolution,
frame_rate,
hdr,
label,
secondary,
} => Stream::Video(VideoStream {
pid: *pid,
codec: str_to_codec(codec),
resolution: resolution.clone(),
@@ -113,10 +140,16 @@ impl M2tsMeta {
color_space: ColorSpace::Bt709,
secondary: *secondary,
label: label.clone(),
})
}
MetaStream::Audio { pid, codec, channels, language, sample_rate, label, secondary } => {
Stream::Audio(AudioStream {
}),
MetaStream::Audio {
pid,
codec,
channels,
language,
sample_rate,
label,
secondary,
} => Stream::Audio(AudioStream {
pid: *pid,
codec: str_to_codec(codec),
channels: channels.clone(),
@@ -124,17 +157,20 @@ impl M2tsMeta {
sample_rate: sample_rate.clone(),
secondary: *secondary,
label: label.clone(),
})
}
MetaStream::Subtitle { pid, codec, language, forced } => {
Stream::Subtitle(SubtitleStream {
}),
MetaStream::Subtitle {
pid,
codec,
language,
forced,
} => Stream::Subtitle(SubtitleStream {
pid: *pid,
codec: str_to_codec(codec),
language: language.clone(),
forced: *forced,
})
}
}).collect();
}),
})
.collect();
DiscTitle {
playlist: self.title.clone(),
@@ -150,12 +186,11 @@ impl M2tsMeta {
/// Write the metadata header to a writer. Padded to 192-byte boundary.
pub fn write_header(w: &mut impl Write, meta: &M2tsMeta) -> io::Result<()> {
let json = serde_json::to_vec(meta)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let json = serde_json::to_vec(meta).map_err(|e| io::Error::other(e))?;
let json_len = json.len() as u32;
let raw_len = 8 + 4 + json.len(); // magic + len + json
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
w.write_all(&MAGIC)?;
@@ -198,7 +233,7 @@ pub fn read_header<R: Read + Seek>(r: &mut R) -> io::Result<Option<M2tsMeta>> {
// Skip padding to next 192-byte boundary
let raw_len = 8 + 4 + json_len;
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
if padding > 0 {
r.seek(SeekFrom::Current(padding as i64))?;
@@ -229,7 +264,7 @@ pub fn read_header_from_stream(r: &mut impl Read) -> io::Result<Option<M2tsMeta>
// Skip padding
let raw_len = 8 + 4 + json_len;
let padded_len = ((raw_len + PACKET_SIZE - 1) / PACKET_SIZE) * PACKET_SIZE;
let padded_len = raw_len.div_ceil(PACKET_SIZE) * PACKET_SIZE;
let padding = padded_len - raw_len;
if padding > 0 {
let mut skip = vec![0u8; padding];
@@ -255,7 +290,8 @@ fn codec_to_str(c: Codec) -> String {
Codec::Lpcm => "lpcm",
Codec::Pgs => "pgs",
Codec::Unknown(_) => "unknown",
}.into()
}
.into()
}
fn str_to_codec(s: &str) -> Codec {
@@ -281,7 +317,8 @@ fn hdr_to_str(h: HdrFormat) -> String {
HdrFormat::Sdr => "sdr",
HdrFormat::Hdr10 => "hdr10",
HdrFormat::DolbyVision => "dv",
}.into()
}
.into()
}
fn str_to_hdr(s: &str) -> HdrFormat {
+62 -23
View File
@@ -4,16 +4,16 @@
//! Designed for streaming writes: clusters are written as data arrives,
//! cues and seek head are finalized at the end.
use std::io::{self, Write, Seek, SeekFrom};
use super::ebml;
use crate::disc::{VideoStream, AudioStream, SubtitleStream, Codec};
use crate::disc::{AudioStream, Codec, SubtitleStream, VideoStream};
use std::io::{self, Seek, SeekFrom, Write};
/// MKV track definition (built from disc stream metadata).
pub struct MkvTrack {
pub track_type: u64, // 1=video, 2=audio, 17=subtitle
pub track_type: u64, // 1=video, 2=audio, 17=subtitle
pub codec_id: &'static str,
pub language: String,
pub name: String, // Track name / label (e.g. "English (Lossless)")
pub name: String, // Track name / label (e.g. "English (Lossless)")
pub codec_private: Option<Vec<u8>>,
pub is_default: bool,
pub is_forced: bool,
@@ -125,7 +125,12 @@ const CLUSTER_DURATION_MS: i64 = 5000;
impl<W: Write + Seek> MkvMuxer<W> {
/// Create a new MKV muxer: writes EBML header, Segment start, Info, Tracks.
pub fn new(mut writer: W, tracks: &[MkvTrack], title: Option<&str>, duration_secs: f64) -> io::Result<Self> {
pub fn new(
mut writer: W,
tracks: &[MkvTrack],
title: Option<&str>,
duration_secs: f64,
) -> io::Result<Self> {
// EBML Header
let ebml_pos = ebml::start_master(&mut writer, ebml::EBML)?;
ebml::write_uint(&mut writer, ebml::EBML_VERSION, 1)?;
@@ -146,7 +151,8 @@ impl<W: Write + Seek> MkvMuxer<W> {
let info_pos = ebml::start_master(&mut writer, ebml::INFO)?;
ebml::write_uint(&mut writer, ebml::TIMESTAMP_SCALE, 1_000_000)?; // 1ms precision
if duration_secs > 0.0 {
ebml::write_float(&mut writer, ebml::DURATION, duration_secs * 1000.0)?; // in ms
ebml::write_float(&mut writer, ebml::DURATION, duration_secs * 1000.0)?;
// in ms
}
ebml::write_string(&mut writer, ebml::MUXING_APP, "freemkv")?;
ebml::write_string(&mut writer, ebml::WRITING_APP, "freemkv")?;
@@ -233,7 +239,13 @@ impl<W: Write + Seek> MkvMuxer<W> {
}
/// Write a single frame.
pub fn write_frame(&mut self, track_idx: usize, pts_ns: i64, keyframe: bool, data: &[u8]) -> io::Result<()> {
pub fn write_frame(
&mut self,
track_idx: usize,
pts_ns: i64,
keyframe: bool,
data: &[u8],
) -> io::Result<()> {
let pts_ms = pts_ns / 1_000_000;
// Start new cluster if needed
@@ -275,7 +287,11 @@ impl<W: Write + Seek> MkvMuxer<W> {
ebml::write_uint(&mut self.writer, ebml::CUE_TIME, cue.timestamp_ms as u64)?;
let ctp_pos = ebml::start_master(&mut self.writer, ebml::CUE_TRACK_POSITIONS)?;
ebml::write_uint(&mut self.writer, ebml::CUE_TRACK, cue.track as u64)?;
ebml::write_uint(&mut self.writer, ebml::CUE_CLUSTER_POSITION, cue.cluster_pos)?;
ebml::write_uint(
&mut self.writer,
ebml::CUE_CLUSTER_POSITION,
cue.cluster_pos,
)?;
ebml::end_master(&mut self.writer, ctp_pos)?;
ebml::end_master(&mut self.writer, cp_pos)?;
}
@@ -331,7 +347,13 @@ impl<W: Write + Seek> MkvMuxer<W> {
Ok(())
}
fn write_simple_block(&mut self, track_num: usize, relative_ts: i16, keyframe: bool, data: &[u8]) -> io::Result<()> {
fn write_simple_block(
&mut self,
track_num: usize,
relative_ts: i16,
keyframe: bool,
data: &[u8],
) -> io::Result<()> {
// SimpleBlock: [track_number VINT] [relative_ts i16] [flags u8] [data]
// Track number as EBML VINT
let track_vint = if track_num < 0x80 {
@@ -359,24 +381,41 @@ impl<W: Write + Seek> MkvMuxer<W> {
// ============================================================
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) }
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("96") { 96000.0 }
else if s.contains("192") { 192000.0 }
else { 48000.0 }
if s.contains("96") {
96000.0
} else if s.contains("192") {
192000.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 }
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
}
}
+150 -48
View File
@@ -3,19 +3,22 @@
//! Write: BD-TS bytes in → demux → codec parse → MKV container out.
//! Read: MKV container in → extract frames → wrap as BD-TS → bytes out.
use std::io::{self, Read, Write, Seek, SeekFrom};
use super::{IOStream, WriteSeek, ReadSeek, ebml};
use super::ts::TsDemuxer;
use super::mkv::{MkvMuxer, MkvTrack};
use super::codec::{self, CodecParser};
use super::lookahead::{LookaheadBuffer, LookaheadState, DEFAULT_LOOKAHEAD_SIZE};
use super::mkv::{MkvMuxer, MkvTrack};
use super::ts::TsDemuxer;
use super::{ebml, IOStream, ReadSeek, WriteSeek};
use crate::disc::*;
use std::io::{self, Read, Seek, SeekFrom, Write};
/// Lookahead buffer for codec header detection (5 MB default).
const DEFAULT_MAX_BUFFER: usize = DEFAULT_LOOKAHEAD_SIZE;
#[derive(Debug, Clone, Copy, PartialEq)]
enum WritePhase { Scanning, Streaming }
enum WritePhase {
Scanning,
Streaming,
}
struct WriteState {
demuxer: TsDemuxer,
@@ -84,9 +87,11 @@ impl MkvStream {
crate::disc::Stream::Audio(a) => {
(a.pid, MkvTrack::audio(a), codec::parser_for_codec(a.codec))
}
crate::disc::Stream::Subtitle(s) => {
(s.pid, MkvTrack::subtitle(s), codec::parser_for_codec(s.codec))
}
crate::disc::Stream::Subtitle(s) => (
s.pid,
MkvTrack::subtitle(s),
codec::parser_for_codec(s.codec),
),
};
let idx = ws.tracks.len();
pids.push(pid);
@@ -128,10 +133,14 @@ impl MkvStream {
}
impl IOStream for MkvStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if self.finished { return Ok(()); }
if self.finished {
return Ok(());
}
self.finished = true;
if let Mode::Write(ref mut ws) = self.mode {
// Flush remaining PES packets
@@ -156,7 +165,12 @@ impl Write for MkvStream {
let dt = &self.disc_title;
let ws = match self.mode {
Mode::Write(ref mut ws) => ws,
Mode::Read(_) => return Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for reading")),
Mode::Read(_) => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for reading",
))
}
};
match ws.phase {
@@ -198,7 +212,9 @@ impl Write for MkvStream {
}
}
fn flush(&mut self) -> io::Result<()> { Ok(()) }
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
// ── Read ───────────────────────────────────────────────────────
@@ -207,7 +223,12 @@ impl Read for MkvStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let rs = match self.mode {
Mode::Read(ref mut rs) => rs,
Mode::Write(_) => return Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for writing")),
Mode::Write(_) => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for writing",
))
}
};
// Drain internal buffer first
@@ -233,10 +254,14 @@ impl Read 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 frame = &block[vl + 3..];
@@ -267,7 +292,9 @@ impl Read for MkvStream {
// ── Write internals ────────────────────────────────────────────
fn check_codec_private(ws: &mut WriteState) -> bool {
if ws.video_pending == 0 { return true; }
if ws.video_pending == 0 {
return true;
}
for (pid, parser) in &ws.parsers {
if let Some(cp) = parser.codec_private() {
if let Some((_, idx)) = ws.pid_to_track.iter().find(|(p, _)| p == pid) {
@@ -282,10 +309,17 @@ fn check_codec_private(ws: &mut WriteState) -> bool {
}
fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
let writer = ws.writer.take()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "writer already consumed"))?;
let writer = ws
.writer
.take()
.ok_or_else(|| io::Error::other("writer already consumed"))?;
ws.muxer = Some(MkvMuxer::new(writer, &ws.tracks, Some(&dt.playlist), dt.duration_secs)?);
ws.muxer = Some(MkvMuxer::new(
writer,
&ws.tracks,
Some(&dt.playlist),
dt.duration_secs,
)?);
ws.phase = WritePhase::Streaming;
// Re-parse buffered data through a fresh demuxer
@@ -332,17 +366,29 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
let mut streams: Vec<crate::disc::Stream> = Vec::new();
let (id, size, _) = ebml::read_element_header(r)?;
if id != ebml::EBML { return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML")); }
if id != ebml::EBML {
return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML"));
}
if size > i64::MAX as u64 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "EBML header too large"));
}
r.seek(SeekFrom::Current(size as i64))?;
let (id, _, _) = ebml::read_element_header(r)?;
if id != ebml::SEGMENT { return Err(io::Error::new(io::ErrorKind::InvalidData, "no Segment")); }
if id != ebml::SEGMENT {
return Err(io::Error::new(io::ErrorKind::InvalidData, "no Segment"));
}
let (mut got_info, mut got_tracks) = (false, false);
loop {
if got_info && got_tracks { break; }
let (id, size, _) = match ebml::read_element_header(r) { Ok(h) => h, Err(_) => break };
if got_info && got_tracks {
break;
}
let (id, size, _) = match ebml::read_element_header(r) {
Ok(h) => h,
Err(_) => break,
};
match id {
ebml::INFO => {
@@ -353,7 +399,9 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
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)?,
_ => { r.seek(SeekFrom::Current(cs as i64))?; }
_ => {
r.seek(SeekFrom::Current(cs as i64))?;
}
}
}
got_info = true;
@@ -363,13 +411,19 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
while r.stream_position()? < end {
let (cid, cs, _) = ebml::read_element_header(r)?;
if cid == ebml::TRACK_ENTRY {
if let Some(s) = parse_track(r, cs)? { streams.push(s); }
} else { r.seek(SeekFrom::Current(cs as i64))?; }
if let Some(s) = parse_track(r, cs)? {
streams.push(s);
}
} else {
r.seek(SeekFrom::Current(cs as i64))?;
}
}
got_tracks = true;
}
ebml::CLUSTER => break,
_ if size != u64::MAX => { r.seek(SeekFrom::Current(size as i64))?; }
_ if size != u64::MAX => {
r.seek(SeekFrom::Current(size as i64))?;
}
_ => break,
}
}
@@ -401,8 +455,11 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
let ve = r.stream_position()? + cs;
while r.stream_position()? < ve {
let (vid, vs, _) = ebml::read_element_header(r)?;
if vid == ebml::PIXEL_HEIGHT { ph = ebml::read_uint_val(r, vs as usize)? as u32; }
else { r.seek(SeekFrom::Current(vs as i64))?; }
if vid == ebml::PIXEL_HEIGHT {
ph = ebml::read_uint_val(r, vs as usize)? as u32;
} else {
r.seek(SeekFrom::Current(vs as i64))?;
}
}
}
ebml::AUDIO => {
@@ -412,37 +469,67 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
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,
_ => { r.seek(SeekFrom::Current(as_ as i64))?; }
_ => {
r.seek(SeekFrom::Current(as_ as i64))?;
}
}
}
}
_ => { r.seek(SeekFrom::Current(cs as i64))?; }
_ => {
r.seek(SeekFrom::Current(cs as i64))?;
}
}
}
let codec = match codec_id.as_str() {
"V_MPEGH/ISO/HEVC" => Codec::Hevc, "V_MPEG4/ISO/AVC" => Codec::H264,
"V_MS/VFW/FOURCC" => Codec::Vc1, "V_MPEG2" => Codec::Mpeg2,
"A_AC3" => Codec::Ac3, "A_EAC3" => Codec::Ac3Plus,
"A_TRUEHD" => Codec::TrueHd, "A_DTS" => Codec::Dts,
"A_PCM/INT/BIG" => Codec::Lpcm, "S_HDMV/PGS" => Codec::Pgs,
"V_MPEGH/ISO/HEVC" => Codec::Hevc,
"V_MPEG4/ISO/AVC" => Codec::H264,
"V_MS/VFW/FOURCC" => Codec::Vc1,
"V_MPEG2" => Codec::Mpeg2,
"A_AC3" => Codec::Ac3,
"A_EAC3" => Codec::Ac3Plus,
"A_TRUEHD" => Codec::TrueHd,
"A_DTS" => Codec::Dts,
"A_PCM/INT/BIG" => Codec::Lpcm,
"S_HDMV/PGS" => Codec::Pgs,
_ => Codec::Unknown(0),
};
let res = format!("{}p", ph);
let chs: String = match ch { 8 => "7.1", 6 => "5.1", 2 => "stereo", 1 => "mono", _ => "5.1" }.into();
let chs: String = match ch {
8 => "7.1",
6 => "5.1",
2 => "stereo",
1 => "mono",
_ => "5.1",
}
.into();
let srs: String = if sr >= 96000.0 { "96kHz" } else { "48kHz" }.into();
Ok(match ttype {
1 => Some(crate::disc::Stream::Video(VideoStream {
pid: tnum, codec, resolution: res, frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709, secondary: false, label: name,
pid: tnum,
codec,
resolution: res,
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: name,
})),
2 => Some(crate::disc::Stream::Audio(AudioStream {
pid: tnum, codec, channels: chs, language: lang, sample_rate: srs,
secondary: false, label: name,
pid: tnum,
codec,
channels: chs,
language: lang,
sample_rate: srs,
secondary: false,
label: name,
})),
17 => Some(crate::disc::Stream::Subtitle(SubtitleStream {
pid: tnum, codec, language: lang, forced,
pid: tnum,
codec,
language: lang,
forced,
})),
_ => None,
})
@@ -451,8 +538,12 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
// ── BD-TS frame wrapping (read side) ──────────────────────────
fn block_vint(d: &[u8]) -> (u64, usize) {
if d.is_empty() { return (0, 0); }
if d[0] & 0x80 != 0 { return ((d[0] & 0x7F) as u64, 1); }
if d.is_empty() {
return (0, 0);
}
if d[0] & 0x80 != 0 {
return ((d[0] & 0x7F) as u64, 1);
}
if d[0] & 0x40 != 0 && d.len() >= 2 {
return ((((d[0] & 0x3F) as u64) << 8) | d[1] as u64, 2);
}
@@ -460,7 +551,11 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
}
fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
let pid = if track == 1 { 0x1011 } else { 0x1100 + (track - 2) as u16 };
let pid = if track == 1 {
0x1011
} else {
0x1100 + (track - 2)
};
let stream_id: u8 = if track == 1 { 0xE0 } else { 0xBD };
let pts = encode_pts(pts_ms * 90);
let hdr = [0x00, 0x00, 0x01, stream_id, 0x00, 0x00, 0x80, 0x80, 0x05];
@@ -476,7 +571,10 @@ fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
let mut pkt = [0u8; 192];
pkt[4] = 0x47;
pkt[5] = (pid >> 8) as u8 & 0x1F;
if pusi { pkt[5] |= 0x40; pusi = false; }
if pusi {
pkt[5] |= 0x40;
pusi = false;
}
pkt[6] = pid as u8;
let space = 184;
@@ -487,8 +585,12 @@ fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
let pad = space - n;
pkt[7] = 0x30; // AF + payload
pkt[8] = pad as u8;
if pad > 1 { pkt[9] = 0x00; }
for i in 10..(8 + pad).min(192) { pkt[i] = 0xFF; }
if pad > 1 {
pkt[9] = 0x00;
}
for i in 10..(8 + pad).min(192) {
pkt[i] = 0xFF;
}
pkt[8 + pad..8 + pad + n].copy_from_slice(&pes[off..off + n]);
} else {
pkt[7] = 0x10; // payload only
+13 -11
View File
@@ -19,32 +19,34 @@
//! output.finish()?;
//! ```
pub mod ebml;
pub mod ts;
pub mod mkv;
pub mod codec;
pub mod disc;
pub mod ebml;
pub mod iso;
mod isowriter;
pub mod lookahead;
pub mod meta;
mod m2ts;
pub mod meta;
pub mod mkv;
mod mkvstream;
pub mod network;
pub mod disc;
pub mod null;
pub mod stdio;
pub mod iso;
pub mod resolve;
pub mod stdio;
pub mod ps;
pub mod ts;
pub use disc::{DiscOptions, DiscStream};
pub use iso::IsoStream;
pub use m2ts::M2tsStream;
pub use mkvstream::MkvStream;
pub use network::NetworkStream;
pub use disc::{DiscStream, DiscOptions};
pub use null::NullStream;
pub use stdio::StdioStream;
pub use iso::IsoStream;
pub use resolve::{open_input, open_output, parse_url, InputOptions, StreamUrl};
pub use stdio::StdioStream;
use std::io::{self, Read, Write, Seek};
use crate::disc::DiscTitle;
use std::io::{self, Read, Seek, Write};
/// Common interface for all stream types.
///
+27 -13
View File
@@ -7,10 +7,10 @@
//! NetworkStream reader can hand off to any output stream (MKV, M2TS, etc.)
//! with full metadata (labels, languages, duration).
use std::io::{self, Read, Write, BufReader, BufWriter};
use std::net::{TcpListener, TcpStream};
use super::{IOStream, meta};
use super::{meta, IOStream};
use crate::disc::DiscTitle;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::net::{TcpListener, TcpStream};
/// I/O buffer size for network reads/writes.
const NET_BUF_SIZE: usize = 256 * 1024;
@@ -37,7 +37,6 @@ impl NetworkStream {
/// Sends FMKV metadata header on first write.
pub fn connect(addr: &str) -> io::Result<Self> {
let stream = TcpStream::connect(addr)?;
stream.set_nodelay(true)?;
Ok(Self {
disc_title: DiscTitle::empty(),
mode: Mode::Write {
@@ -64,10 +63,12 @@ impl NetworkStream {
// Read FMKV metadata header (inline, since TcpStream doesn't impl Seek)
let disc_title = meta::read_header_from_stream(&mut reader)?
.ok_or_else(|| io::Error::new(
io::ErrorKind::InvalidData,
"no FMKV metadata header from sender",
))?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"no FMKV metadata header from sender",
)
})?
.to_title();
Ok(Self {
@@ -79,10 +80,14 @@ impl NetworkStream {
}
impl IOStream for NetworkStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if self.finished { return Ok(()); }
if self.finished {
return Ok(());
}
self.finished = true;
if let Mode::Write { ref mut writer, .. } = self.mode {
writer.flush()?;
@@ -95,7 +100,10 @@ impl IOStream for NetworkStream {
impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.mode {
Mode::Write { ref mut writer, ref mut header_written } => {
Mode::Write {
ref mut writer,
ref mut header_written,
} => {
if !*header_written {
if !self.disc_title.streams.is_empty() {
let m = meta::M2tsMeta::from_title(&self.disc_title);
@@ -105,7 +113,10 @@ impl Write for NetworkStream {
}
writer.write(buf)
}
Mode::Read { .. } => Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for reading")),
Mode::Read { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for reading",
)),
}
}
@@ -122,7 +133,10 @@ impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.mode {
Mode::Read { ref mut reader } => reader.read(buf),
Mode::Write { .. } => Err(io::Error::new(io::ErrorKind::Unsupported, "stream opened for writing")),
Mode::Write { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stream opened for writing",
)),
}
}
}
+27 -7
View File
@@ -1,8 +1,8 @@
//! NullStream — discards all data. Write-only. For benchmarking.
use std::io::{self, Read, Write};
use super::IOStream;
use crate::disc::DiscTitle;
use std::io::{self, Read, Write};
/// Null stream — accepts writes, discards data. For benchmarking rip speed.
pub struct NullStream {
@@ -10,9 +10,18 @@ pub struct NullStream {
bytes_written: u64,
}
impl Default for NullStream {
fn default() -> Self {
Self::new()
}
}
impl NullStream {
pub fn new() -> Self {
Self { disc_title: DiscTitle::empty(), bytes_written: 0 }
Self {
disc_title: DiscTitle::empty(),
bytes_written: 0,
}
}
pub fn meta(mut self, dt: &DiscTitle) -> Self {
@@ -20,12 +29,18 @@ impl NullStream {
self
}
pub fn bytes_written(&self) -> u64 { self.bytes_written }
pub fn bytes_written(&self) -> u64 {
self.bytes_written
}
}
impl IOStream for NullStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Write for NullStream {
@@ -33,12 +48,17 @@ impl Write for NullStream {
self.bytes_written += buf.len() as u64;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> { Ok(()) }
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Read for NullStream {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::Unsupported, "null stream is write-only"))
Err(io::Error::new(
io::ErrorKind::Unsupported,
"null stream is write-only",
))
}
}
+576
View File
@@ -0,0 +1,576 @@
//! MPEG-2 Program Stream (PS) demuxer.
//!
//! DVDs use MPEG-2 Program Stream, which has:
//! - Pack headers (00 00 01 BA) with SCR timestamps
//! - PES packets (00 00 01 [stream_id]) with variable length
//! - System headers (00 00 01 BB)
//! - Program end code (00 00 01 B9)
//!
//! Stream IDs:
//! - 0xE0-0xEF: video (usually 0xE0)
//! - 0xC0-0xDF: MPEG audio
//! - 0xBD: private stream 1 (AC3, DTS, LPCM, subtitles via sub-stream ID)
/// Pack header start code suffix.
const PACK_HEADER_ID: u8 = 0xBA;
/// System header start code suffix.
const SYSTEM_HEADER_ID: u8 = 0xBB;
/// Program end start code suffix.
const PROGRAM_END_ID: u8 = 0xB9;
/// Private stream 1 (AC3, DTS, LPCM, subtitles).
const PRIVATE_STREAM_1: u8 = 0xBD;
/// A demuxed PES packet from the Program Stream.
#[derive(Debug, Clone)]
pub struct PsPacket {
/// PES stream ID (0xE0 for video, 0xC0 for audio, 0xBD for private, etc.).
pub stream_id: u8,
/// Sub-stream ID for private stream 1 (AC3: 0x80-0x87, DTS: 0x88-0x8F,
/// LPCM: 0xA0-0xA7, subtitles: 0x20-0x3F).
pub sub_stream_id: Option<u8>,
/// Presentation timestamp in 90kHz ticks.
pub pts: Option<u64>,
/// Decode timestamp in 90kHz ticks.
pub dts: Option<u64>,
/// Elementary stream payload data.
pub data: Vec<u8>,
}
/// MPEG-2 Program Stream demuxer.
///
/// Accepts raw PS bytes via `feed()` and produces demuxed PES packets.
/// Handles non-aligned input by buffering leftover bytes between calls.
pub struct PsDemuxer {
buffer: Vec<u8>,
}
impl Default for PsDemuxer {
fn default() -> Self {
Self::new()
}
}
impl PsDemuxer {
/// Create a new Program Stream demuxer.
pub fn new() -> Self {
Self {
buffer: Vec::with_capacity(64 * 1024),
}
}
/// Feed raw MPEG-2 PS bytes, returning any completely parsed PES packets.
pub fn feed(&mut self, data: &[u8]) -> Vec<PsPacket> {
self.buffer.extend_from_slice(data);
self.extract_packets()
}
/// Flush remaining buffered data, returning any final PES packets.
pub fn flush(&mut self) -> Vec<PsPacket> {
// Try to extract whatever remains. If the buffer contains an incomplete
// PES packet we cannot parse, it will be discarded.
let packets = self.extract_packets();
self.buffer.clear();
packets
}
/// Scan the buffer for complete start-code-delimited units and parse them.
fn extract_packets(&mut self) -> Vec<PsPacket> {
let mut packets = Vec::new();
let mut pos = 0;
loop {
// Find the next start code.
let sc = match find_start_code(&self.buffer, pos) {
Some(p) => p,
None => break,
};
if sc + 3 >= self.buffer.len() {
// Not enough bytes to read the start code ID.
break;
}
let code = self.buffer[sc + 3];
match code {
PROGRAM_END_ID => {
// 00 00 01 B9 — 4 bytes, no payload.
pos = sc + 4;
}
PACK_HEADER_ID => {
// Pack header: need at least 14 bytes for MPEG-2 pack.
if sc + 14 > self.buffer.len() {
break; // wait for more data
}
// MPEG-2 packs have bit pattern 01 in bits 7-6 of byte 4.
let stuffing = (self.buffer[sc + 13] & 0x07) as usize;
let pack_len = 14 + stuffing;
if sc + pack_len > self.buffer.len() {
break;
}
pos = sc + pack_len;
}
SYSTEM_HEADER_ID => {
// System header: 00 00 01 BB [length:2] ...
if sc + 6 > self.buffer.len() {
break;
}
let header_len = ((self.buffer[sc + 4] as usize) << 8)
| self.buffer[sc + 5] as usize;
let total = 6 + header_len;
if sc + total > self.buffer.len() {
break;
}
pos = sc + total;
}
id if is_pes_stream_id(id) => {
// PES packet: 00 00 01 [stream_id] [length:2] ...
if sc + 6 > self.buffer.len() {
break;
}
let pes_packet_len = ((self.buffer[sc + 4] as usize) << 8)
| self.buffer[sc + 5] as usize;
// Total bytes = 6 (start code + stream_id + length) + pes_packet_len.
// A length of 0 means unbounded (video streams); in that case we need
// to find the next start code to delimit the packet.
let end = if pes_packet_len == 0 {
// Find the next start code after this one.
match find_start_code(&self.buffer, sc + 4) {
Some(next_sc) => next_sc,
None => break, // wait for more data
}
} else {
let e = sc + 6 + pes_packet_len;
if e > self.buffer.len() {
break; // wait for more data
}
e
};
if let Some(pkt) = parse_pes_packet(&self.buffer[sc..end]) {
packets.push(pkt);
}
pos = end;
}
_ => {
// Unknown start code — skip past it.
pos = sc + 4;
}
}
}
if pos > 0 {
self.buffer.drain(..pos);
}
packets
}
}
/// Check whether a start code byte is a valid PES stream ID that carries payload.
fn is_pes_stream_id(id: u8) -> bool {
// Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD,
// private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc.
// We parse anything in the PES range.
matches!(id, 0xBD | 0xBE | 0xBF | 0xC0..=0xEF)
}
/// Parse a single PES packet from a byte slice that starts at the start code.
fn parse_pes_packet(data: &[u8]) -> Option<PsPacket> {
// Minimum: 00 00 01 [id] [len:2] = 6 bytes
if data.len() < 6 {
return None;
}
if data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x01 {
return None;
}
let stream_id = data[3];
// Padding stream — skip entirely.
if stream_id == 0xBE {
return None;
}
// Streams without standard PES header extension.
if stream_id == 0xBF {
let payload = if data.len() > 6 { &data[6..] } else { &[] };
return Some(PsPacket {
stream_id,
sub_stream_id: None,
pts: None,
dts: None,
data: payload.to_vec(),
});
}
// Standard PES header: [6]=flags1, [7]=flags2, [8]=header_data_length
if data.len() < 9 {
return None;
}
let pts_dts_flags = (data[7] >> 6) & 0x03;
let header_data_len = data[8] as usize;
let header_end = 9 + header_data_len;
if header_end > data.len() {
return None;
}
let mut pts = None;
let mut dts = None;
if pts_dts_flags >= 2 && data.len() >= 14 {
pts = Some(parse_pts(&data[9..14]));
}
if pts_dts_flags == 3 && data.len() >= 19 {
dts = Some(parse_pts(&data[14..19]));
}
let payload = &data[header_end..];
// For private stream 1, the first payload byte is the sub-stream ID.
let (sub_stream_id, es_data) = if stream_id == PRIVATE_STREAM_1 && !payload.is_empty() {
(Some(payload[0]), payload[1..].to_vec())
} else {
(None, payload.to_vec())
};
Some(PsPacket {
stream_id,
sub_stream_id,
pts,
dts,
data: es_data,
})
}
/// Parse a 5-byte PTS/DTS timestamp field (33 bits at 90kHz).
///
/// Layout:
/// ```text
/// byte0: [marker_4bits][bit32][marker_1]
/// byte1: [bits 31..24]
/// byte2: [bits 23..15][marker_1]
/// byte3: [bits 14..7]
/// byte4: [bits 6..0][marker_1]
/// ```
fn parse_pts(buf: &[u8]) -> u64 {
debug_assert!(buf.len() >= 5);
let b0 = buf[0] as u64;
let b1 = buf[1] as u64;
let b2 = buf[2] as u64;
let b3 = buf[3] as u64;
let b4 = buf[4] as u64;
((b0 >> 1) & 0x07) << 30
| b1 << 22
| (b2 >> 1) << 15
| b3 << 7
| b4 >> 1
}
/// Find the position of the next start code (00 00 01) at or after `from`.
fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
if data.len() < from + 3 {
return None;
}
for i in from..data.len() - 2 {
if data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01 {
return Some(i);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
// --- Pack header detection ---
#[test]
fn detect_pack_header() {
let mut demuxer = PsDemuxer::new();
// MPEG-2 pack header: 14 bytes, stuffing_length = 0
let mut pack = vec![
0x00, 0x00, 0x01, 0xBA, // start code
0x44, 0x00, 0x04, 0x00, 0x04, 0x01, // SCR (6 bytes)
0x01, 0x89, 0xC3, // mux_rate (3 bytes)
0xF8, // stuffing_length = 0 (lower 3 bits)
];
// Follow with a PES packet so we have a delimiter
pack.extend_from_slice(&[
0x00, 0x00, 0x01, 0xE0, // video stream
0x00, 0x08, // length = 8
0x80, 0x00, 0x00, // flags: no PTS/DTS, header_data_length = 0
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, // payload (5 bytes)
]);
let packets = demuxer.feed(&pack);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].stream_id, 0xE0);
assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]);
}
#[test]
fn pack_header_with_stuffing() {
let mut demuxer = PsDemuxer::new();
// Pack header with 3 stuffing bytes
let mut data = vec![
0x00, 0x00, 0x01, 0xBA,
0x44, 0x00, 0x04, 0x00, 0x04, 0x01,
0x01, 0x89, 0xC3,
0xFB, // stuffing_length = 3
0xFF, 0xFF, 0xFF, // stuffing bytes
];
// Followed by a PES packet
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xC0, // audio stream
0x00, 0x05, // length = 5
0x80, 0x00, 0x00, // flags: no PTS, header_data_len=0
0x11, 0x22, // payload
]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].stream_id, 0xC0);
assert_eq!(packets[0].data, vec![0x11, 0x22]);
}
// --- PES header + PTS parsing ---
#[test]
fn pes_header_with_pts() {
let mut demuxer = PsDemuxer::new();
// PTS = 90000 (1 second at 90kHz)
// 90000 = 0x15F90
// bit32=0, bits 29-15 = 0x0002BF, bits 14-0 = 0x1F90
// byte0: 0010_0_1 = 0x21 ... actually let's encode properly:
//
// pts = 90000
// byte0: (0010 << 4) | ((pts >> 29) & 0x0E) | 1
// = 0x20 | ((90000 >> 29) & 0x0E) | 1 = 0x20 | 0 | 1 = 0x21
// byte1: (pts >> 22) & 0xFF = (90000 >> 22) & 0xFF = 0
// byte2: ((pts >> 14) & 0xFE) | 1 = ((90000 >> 14) & 0xFE) | 1 = (0x0A & 0xFE) | 1 = 0x0B
// byte3: (pts >> 7) & 0xFF = (90000 >> 7) & 0xFF = (703) & 0xFF = 0xBF
// byte4: ((pts & 0x7F) << 1) | 1 = ((90000 & 0x7F) << 1) | 1 = (0x10 << 1) | 1 = 0x21
let pts_bytes = encode_pts(90000, 0x20);
let mut data = vec![
0x00, 0x00, 0x01, 0xE0, // video stream
0x00, 0x0D, // length = 13
0x80, 0x80, 0x05, // flags: PTS only, header_data_len=5
];
data.extend_from_slice(&pts_bytes);
data.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x00]); // payload
// Add a delimiter
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); // program end
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].stream_id, 0xE0);
assert_eq!(packets[0].pts, Some(90000));
assert!(packets[0].dts.is_none());
assert_eq!(packets[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00]);
}
#[test]
fn pes_header_with_pts_and_dts() {
let mut demuxer = PsDemuxer::new();
let pts_bytes = encode_pts(180000, 0x30); // PTS marker = 0x30
let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10
let mut data = vec![
0x00, 0x00, 0x01, 0xE0,
0x00, 0x11, // length = 17
0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10
];
data.extend_from_slice(&pts_bytes);
data.extend_from_slice(&dts_bytes);
data.extend_from_slice(&[0xCA, 0xFE]); // payload
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].pts, Some(180000));
assert_eq!(packets[0].dts, Some(90000));
}
// --- Private stream 1 sub-stream extraction ---
#[test]
fn private_stream_1_ac3_substream() {
let mut demuxer = PsDemuxer::new();
let mut data = vec![
0x00, 0x00, 0x01, 0xBD, // private stream 1
0x00, 0x08, // length = 8
0x80, 0x00, 0x00, // no PTS, header_data_len=0
0x80, // sub-stream ID: AC3 stream 0
0xAA, 0xBB, 0xCC, 0xDD, // AC3 payload
];
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].stream_id, 0xBD);
assert_eq!(packets[0].sub_stream_id, Some(0x80));
assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn private_stream_1_dts_substream() {
let mut demuxer = PsDemuxer::new();
let mut data = vec![
0x00, 0x00, 0x01, 0xBD,
0x00, 0x06, // length = 6
0x80, 0x00, 0x00,
0x88, // sub-stream ID: DTS stream 0
0x11, 0x22,
];
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].sub_stream_id, Some(0x88));
}
#[test]
fn private_stream_1_subtitle_substream() {
let mut demuxer = PsDemuxer::new();
let mut data = vec![
0x00, 0x00, 0x01, 0xBD,
0x00, 0x06,
0x80, 0x00, 0x00,
0x20, // sub-stream ID: subtitle stream 0
0xFF, 0xFE,
];
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].sub_stream_id, Some(0x20));
}
#[test]
fn private_stream_1_lpcm_substream() {
let mut demuxer = PsDemuxer::new();
let mut data = vec![
0x00, 0x00, 0x01, 0xBD,
0x00, 0x06,
0x80, 0x00, 0x00,
0xA0, // sub-stream ID: LPCM stream 0
0x01, 0x02,
];
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].sub_stream_id, Some(0xA0));
}
// --- Incremental feeding ---
#[test]
fn incremental_feed() {
let mut demuxer = PsDemuxer::new();
let mut full = vec![
0x00, 0x00, 0x01, 0xE0,
0x00, 0x06, // length = 6
0x80, 0x00, 0x00, // no PTS, header_data_len=0
0xAA, 0xBB, 0xCC,
];
full.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
// Feed in two halves
let mid = full.len() / 2;
let p1 = demuxer.feed(&full[..mid]);
assert!(p1.is_empty(), "first half should not produce packets");
let p2 = demuxer.feed(&full[mid..]);
assert_eq!(p2.len(), 1);
assert_eq!(p2[0].data, vec![0xAA, 0xBB, 0xCC]);
}
// --- Multiple PES packets ---
#[test]
fn multiple_pes_packets() {
let mut demuxer = PsDemuxer::new();
let mut data = Vec::new();
// First PES: video
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xE0,
0x00, 0x05,
0x80, 0x00, 0x00,
0x11, 0x22,
]);
// Second PES: audio
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xC0,
0x00, 0x05,
0x80, 0x00, 0x00,
0x33, 0x44,
]);
// Delimiter
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
let packets = demuxer.feed(&data);
assert_eq!(packets.len(), 2);
assert_eq!(packets[0].stream_id, 0xE0);
assert_eq!(packets[1].stream_id, 0xC0);
}
// --- PTS parsing edge cases ---
#[test]
fn pts_zero() {
// PTS = 0 encoded
let pts = parse_pts(&encode_pts(0, 0x20));
assert_eq!(pts, 0);
}
#[test]
fn pts_large_value() {
// Test a large PTS value (close to 33-bit max)
let val: u64 = (1 << 32) - 1; // 0xFFFFFFFF
let encoded = encode_pts(val, 0x20);
let decoded = parse_pts(&encoded);
assert_eq!(decoded, val);
}
// --- Helper: encode PTS for tests ---
fn encode_pts(pts: u64, marker_prefix: u8) -> [u8; 5] {
let mut buf = [0u8; 5];
buf[0] = marker_prefix | (((pts >> 30) as u8) & 0x07) << 1 | 1;
buf[1] = ((pts >> 22) & 0xFF) as u8;
buf[2] = (((pts >> 15) & 0x7F) as u8) << 1 | 1;
buf[3] = ((pts >> 7) & 0xFF) as u8;
buf[4] = (((pts) & 0x7F) as u8) << 1 | 1;
buf
}
}
+64 -27
View File
@@ -14,15 +14,15 @@
//!
//! Bare paths without a scheme are rejected.
use std::io::{self, BufReader, BufWriter};
use std::path::Path;
use super::{IOStream, M2tsStream, MkvStream};
use super::disc::{DiscOptions, DiscStream};
use super::iso::IsoStream;
use super::network::NetworkStream;
use super::null::NullStream;
use super::stdio::StdioStream;
use super::iso::IsoStream;
use super::disc::{DiscStream, DiscOptions};
use super::{IOStream, M2tsStream, MkvStream};
use crate::disc::DiscTitle;
use std::io::{self, BufReader, BufWriter};
use std::path::Path;
/// I/O buffer size for file streams.
const IO_BUF_SIZE: usize = 4 * 1024 * 1024;
@@ -50,40 +50,74 @@ pub struct StreamUrl {
/// ```
pub fn parse_url(url: &str) -> StreamUrl {
if let Some(rest) = url.strip_prefix("disc://") {
return StreamUrl { scheme: "disc".into(), path: rest.to_string() };
return StreamUrl {
scheme: "disc".into(),
path: rest.to_string(),
};
}
if let Some(rest) = url.strip_prefix("m2ts://") {
return StreamUrl { scheme: "m2ts".into(), path: rest.to_string() };
return StreamUrl {
scheme: "m2ts".into(),
path: rest.to_string(),
};
}
if let Some(rest) = url.strip_prefix("mkv://") {
return StreamUrl { scheme: "mkv".into(), path: rest.to_string() };
return StreamUrl {
scheme: "mkv".into(),
path: rest.to_string(),
};
}
if let Some(rest) = url.strip_prefix("network://") {
return StreamUrl { scheme: "network".into(), path: rest.to_string() };
return StreamUrl {
scheme: "network".into(),
path: rest.to_string(),
};
}
if url == "null://" || url.starts_with("null://") {
return StreamUrl { scheme: "null".into(), path: String::new() };
return StreamUrl {
scheme: "null".into(),
path: String::new(),
};
}
if url == "stdio://" || url.starts_with("stdio://") {
return StreamUrl { scheme: "stdio".into(), path: String::new() };
return StreamUrl {
scheme: "stdio".into(),
path: String::new(),
};
}
if let Some(rest) = url.strip_prefix("iso://") {
return StreamUrl { scheme: "iso".into(), path: rest.to_string() };
return StreamUrl {
scheme: "iso".into(),
path: rest.to_string(),
};
}
StreamUrl { scheme: "unknown".into(), path: url.to_string() }
StreamUrl {
scheme: "unknown".into(),
path: url.to_string(),
}
}
/// Validate that a file path is non-empty and has a filename component.
fn validate_file_path(path: &str, scheme: &str) -> io::Result<()> {
if path.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("{}:// requires a file path (e.g. {}://movie.{})", scheme, scheme, scheme)));
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}:// requires a file path (e.g. {}://movie.{})",
scheme, scheme, scheme
),
));
}
let p = Path::new(path);
if p.file_name().is_none() {
return Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("{}://{} is not a valid file path — must include a filename", scheme, path)));
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{}://{} is not a valid file path — must include a filename",
scheme, path
),
));
}
Ok(())
}
@@ -91,12 +125,19 @@ fn validate_file_path(path: &str, scheme: &str) -> io::Result<()> {
/// Validate that a network address has host:port format.
fn validate_network_addr(addr: &str) -> io::Result<()> {
if addr.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput,
"network:// requires host:port (e.g. network://0.0.0.0:9000)"));
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"network:// requires host:port (e.g. network://0.0.0.0:9000)",
));
}
if !addr.contains(':') {
return Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("network://{} missing port — use network://{}:PORT", addr, addr)));
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"network://{} missing port — use network://{}:PORT",
addr, addr
),
));
}
Ok(())
}
@@ -113,7 +154,7 @@ pub fn open_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn IOStream
title_index: opts.title_index,
};
let stream = DiscStream::open(disc_opts)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(Box::new(stream))
}
"m2ts" => {
@@ -209,13 +250,9 @@ pub fn open_output(url: &str, meta: &DiscTitle) -> io::Result<Box<dyn IOStream>>
}
/// Options for opening an input stream.
#[derive(Default)]
pub struct InputOptions {
pub keydb_path: Option<String>,
pub title_index: Option<usize>,
}
impl Default for InputOptions {
fn default() -> Self {
Self { keydb_path: None, title_index: None }
}
}
+12 -6
View File
@@ -1,8 +1,8 @@
//! StdioStream — raw byte pipe via stdin/stdout. Format-agnostic.
use std::io::{self, Read, Write};
use super::IOStream;
use crate::disc::DiscTitle;
use std::io::{self, Read, Write};
/// Stdio stream — reads from stdin, writes to stdout.
///
@@ -41,7 +41,9 @@ impl StdioStream {
}
impl IOStream for StdioStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn info(&self) -> &DiscTitle {
&self.disc_title
}
fn finish(&mut self) -> io::Result<()> {
if let Some(ref mut w) = self.writer {
w.flush()?;
@@ -54,8 +56,10 @@ impl Read for StdioStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.reader {
Some(ref mut r) => r.read(buf),
None => Err(io::Error::new(io::ErrorKind::Unsupported,
"stdio:// opened for output — cannot read")),
None => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stdio:// opened for output — cannot read",
)),
}
}
}
@@ -64,8 +68,10 @@ impl Write for StdioStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.writer {
Some(ref mut w) => w.write(buf),
None => Err(io::Error::new(io::ErrorKind::Unsupported,
"stdio:// opened for input — cannot write")),
None => Err(io::Error::new(
io::ErrorKind::Unsupported,
"stdio:// opened for input — cannot write",
)),
}
}
fn flush(&mut self) -> io::Result<()> {
+124 -54
View File
@@ -100,7 +100,7 @@ impl PesAssembler {
pub struct TsDemuxer {
assemblers: Vec<PesAssembler>,
pid_index: [i16; 8192], // PID → index into assemblers, -1 = not tracked
remainder: Vec<u8>, // leftover bytes from previous feed() call
remainder: Vec<u8>, // leftover bytes from previous feed() call
}
impl TsDemuxer {
@@ -112,7 +112,11 @@ impl TsDemuxer {
pid_index[pid as usize] = i as i16;
assemblers.push(PesAssembler::new(pid));
}
Self { assemblers, pid_index, remainder: Vec::new() }
Self {
assemblers,
pid_index,
remainder: Vec::new(),
}
}
/// Feed a chunk of BD transport stream data. Handles non-192-byte-aligned input
@@ -225,8 +229,12 @@ fn parse_pes_header(data: &[u8]) -> (Option<i64>, Option<i64>, usize) {
// Some stream IDs don't have the standard PES header extension
// (program_stream_map, padding, private_stream_2, ECM, EMM, etc.)
if stream_id == 0xBC || stream_id == 0xBE || stream_id == 0xBF
|| stream_id == 0xF0 || stream_id == 0xF1 || stream_id == 0xFF
if stream_id == 0xBC
|| stream_id == 0xBE
|| stream_id == 0xBF
|| stream_id == 0xF0
|| stream_id == 0xF1
|| stream_id == 0xFF
{
return (None, None, 6);
}
@@ -261,11 +269,7 @@ fn parse_timestamp(data: &[u8]) -> i64 {
let b3 = data[3] as i64;
let b4 = data[4] as i64;
((b0 >> 1) & 0x07) << 30
| b1 << 22
| (b2 >> 1) << 15
| b3 << 7
| b4 >> 1
((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1
}
// ============================================================
@@ -281,7 +285,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let mut pat_pmt_pid: Option<u16> = None;
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
@@ -291,7 +298,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let pointer = data[payload_start] as usize;
let pat_start = payload_start + 1 + pointer;
if pat_start + 12 < data.len() && data[pat_start] == 0x00 {
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8) | data[pat_start + 2] as usize;
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
| data[pat_start + 2] as usize;
let entries_start = pat_start + 8;
let entries_end = pat_start + 3 + section_len - 4;
let mut e = entries_start;
@@ -316,20 +324,34 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let mut streams = Vec::new();
offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == pmt_pid && pusi {
let payload_start = offset + 4 + 4;
if payload_start + 1 >= data.len() { offset += BD_TS_PACKET_SIZE; continue; }
if payload_start + 1 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
let pointer = data[payload_start] as usize;
let pmt_start = payload_start + 1 + pointer;
if pmt_start + 12 >= data.len() { offset += BD_TS_PACKET_SIZE; continue; }
if data[pmt_start] != 0x02 { offset += BD_TS_PACKET_SIZE; continue; }
if pmt_start + 12 >= data.len() {
offset += BD_TS_PACKET_SIZE;
continue;
}
if data[pmt_start] != 0x02 {
offset += BD_TS_PACKET_SIZE;
continue;
}
let section_len = (((data[pmt_start + 1] & 0x0F) as usize) << 8) | data[pmt_start + 2] as usize;
let prog_info_len = (((data[pmt_start + 10] & 0x0F) as usize) << 8) | data[pmt_start + 11] as usize;
let section_len =
(((data[pmt_start + 1] & 0x0F) as usize) << 8) | data[pmt_start + 2] as usize;
let prog_info_len =
(((data[pmt_start + 10] & 0x0F) as usize) << 8) | data[pmt_start + 11] as usize;
let mut pos = pmt_start + 12 + prog_info_len;
let end = pmt_start + 3 + section_len - 4;
@@ -340,57 +362,95 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
let stream = match stream_type {
0x1B => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::H264,
resolution: "1080p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::H264,
resolution: "1080p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x24 => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Hevc,
resolution: "2160p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Hevc,
resolution: "2160p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0xEA => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Vc1,
resolution: "1080p".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Vc1,
resolution: "1080p".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x02 => Some(Stream::Video(VideoStream {
pid: es_pid, codec: Codec::Mpeg2,
resolution: "1080i".into(), frame_rate: String::new(),
hdr: HdrFormat::Sdr, color_space: ColorSpace::Bt709,
secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Mpeg2,
resolution: "1080i".into(),
frame_rate: String::new(),
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
secondary: false,
label: String::new(),
})),
0x81 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Ac3,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Ac3,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x83 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::TrueHd,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::TrueHd,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x84 | 0xA1 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Ac3Plus,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Ac3Plus,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x85 | 0x86 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::DtsHdMa,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::DtsHdMa,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x82 => Some(Stream::Audio(AudioStream {
pid: es_pid, codec: Codec::Dts,
channels: "5.1".into(), language: "und".into(),
sample_rate: "48kHz".into(), secondary: false, label: String::new(),
pid: es_pid,
codec: Codec::Dts,
channels: "5.1".into(),
language: "und".into(),
sample_rate: "48kHz".into(),
secondary: false,
label: String::new(),
})),
0x90 => Some(Stream::Subtitle(SubtitleStream {
pid: es_pid, codec: Codec::Pgs,
language: "und".into(), forced: false,
pid: es_pid,
codec: Codec::Pgs,
language: "und".into(),
forced: false,
})),
_ => None,
};
@@ -405,7 +465,11 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
offset += BD_TS_PACKET_SIZE;
}
if streams.is_empty() { None } else { Some(streams) }
if streams.is_empty() {
None
} else {
Some(streams)
}
}
// ============================================================
@@ -416,7 +480,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
pub fn scan_first_pts(data: &[u8], target_pid: u16) -> Option<i64> {
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == target_pid && pusi {
@@ -443,7 +510,10 @@ pub fn scan_last_pts(data: &[u8], target_pid: u16) -> Option<i64> {
let mut last_pts = None;
let mut offset = 0;
while offset + BD_TS_PACKET_SIZE <= data.len() {
if data[offset + 4] != SYNC_BYTE { offset += 1; continue; }
if data[offset + 4] != SYNC_BYTE {
offset += 1;
continue;
}
let pid = (((data[offset + 5] & 0x1F) as u16) << 8) | data[offset + 6] as u16;
let pusi = data[offset + 5] & 0x40 != 0;
if pid == target_pid && pusi {
@@ -482,7 +552,7 @@ pub fn scan_duration<R: std::io::Read + std::io::Seek>(r: &mut R, video_pid: u16
// Read last 2MB for last PTS (aligned to 192-byte boundary)
let file_size = r.seek(SeekFrom::End(0)).ok()?;
let tail_size: u64 = SCAN_TAIL_SIZE as u64;
let raw_pos = if file_size > tail_size { file_size - tail_size } else { 0 };
let raw_pos = file_size.saturating_sub(tail_size);
let seek_pos = (raw_pos / BD_TS_PACKET_SIZE as u64) * BD_TS_PACKET_SIZE as u64;
r.seek(SeekFrom::Start(seek_pos)).ok()?;
let mut tail_buf = vec![0u8; tail_size as usize];