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:
+13
-2
@@ -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
@@ -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
@@ -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
@@ -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 ---
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(§or).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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(§or.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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user