From 0b014287a346244d2270edb864d1e03f10361ff8 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:19:40 -0700 Subject: [PATCH] Add MKV muxer and event system - Add event.rs: structured event system for progress reporting - Add mux/: MKV muxer pipeline (EBML writer, TS demuxer, stream assembly) - Codec parsers: H.264, HEVC, AC-3, DTS, TrueHD, PGS, VC-1 - Lookahead buffer for codec private data extraction - Direct m2ts-to-MKV streaming without intermediate files --- src/event.rs | 76 ++++++++ src/mux/codec/ac3.rs | 37 ++++ src/mux/codec/dts.rs | 26 +++ src/mux/codec/h264.rs | 192 ++++++++++++++++++++ src/mux/codec/hevc.rs | 170 ++++++++++++++++++ src/mux/codec/mod.rs | 88 ++++++++++ src/mux/codec/pgs.rs | 25 +++ src/mux/codec/truehd.rs | 27 +++ src/mux/codec/vc1.rs | 115 ++++++++++++ src/mux/ebml.rs | 272 +++++++++++++++++++++++++++++ src/mux/lookahead.rs | 127 ++++++++++++++ src/mux/mkv.rs | 375 ++++++++++++++++++++++++++++++++++++++++ src/mux/mod.rs | 25 +++ src/mux/stream.rs | 218 +++++++++++++++++++++++ src/mux/ts.rs | 269 ++++++++++++++++++++++++++++ 15 files changed, 2042 insertions(+) create mode 100644 src/event.rs create mode 100644 src/mux/codec/ac3.rs create mode 100644 src/mux/codec/dts.rs create mode 100644 src/mux/codec/h264.rs create mode 100644 src/mux/codec/hevc.rs create mode 100644 src/mux/codec/mod.rs create mode 100644 src/mux/codec/pgs.rs create mode 100644 src/mux/codec/truehd.rs create mode 100644 src/mux/codec/vc1.rs create mode 100644 src/mux/ebml.rs create mode 100644 src/mux/lookahead.rs create mode 100644 src/mux/mkv.rs create mode 100644 src/mux/mod.rs create mode 100644 src/mux/stream.rs create mode 100644 src/mux/ts.rs diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..0ad73c7 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,76 @@ +//! Event system for progress and status reporting. +//! +//! The lib fires events during operations like rip(). +//! The app registers a callback to receive them. +//! No display logic, no English text — just data. +//! +//! ```rust +//! disc.rip(&mut session, 0, output, |event| { +//! match event.kind { +//! EventKind::BytesRead { bytes, total } => update_progress(bytes, total), +//! EventKind::ReadError { sector, .. } => log_error(sector), +//! _ => {} +//! } +//! }); +//! ``` + +use crate::error::Error; + +/// An event fired by the lib during operations. +#[derive(Debug)] +pub struct Event { + pub kind: EventKind, +} + +/// Types of events the lib can fire. +#[derive(Debug)] +pub enum EventKind { + /// Bytes successfully read and written to output. + BytesRead { + /// Bytes written so far. + bytes: u64, + /// Total bytes expected (0 if unknown). + total: u64, + }, + + /// A read error occurred. The lib will retry automatically. + ReadError { + /// Sector that failed. + sector: u64, + /// Error code. + error: Error, + }, + + /// Retrying a failed read. + Retry { + /// Current attempt number (1-based). + attempt: u32, + }, + + /// Drive speed changed (error recovery or restoration). + SpeedChange { + /// New speed in KB/s (0xFFFF = max). + speed_kbs: u16, + }, + + /// Starting a new disc extent. + ExtentStart { + /// Extent index (0-based). + index: usize, + /// First sector of extent. + start_sector: u64, + /// Number of sectors in extent. + sector_count: u64, + }, + + /// Operation complete. + Complete { + /// Total bytes written. + bytes: u64, + /// Total read errors encountered. + errors: u32, + }, +} + +/// A no-op event handler. Ignores all events. +pub fn ignore(_event: Event) {} diff --git a/src/mux/codec/ac3.rs b/src/mux/codec/ac3.rs new file mode 100644 index 0000000..71a2cd6 --- /dev/null +++ b/src/mux/codec/ac3.rs @@ -0,0 +1,37 @@ +//! AC3 (Dolby Digital) / EAC3 (Dolby Digital Plus) frame parser. +//! +//! AC3 frames are self-contained and always start with syncword 0x0B77. +//! 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}; + +pub struct Ac3Parser; + +impl Ac3Parser { + pub fn new() -> Self { + Self + } +} + +impl CodecParser for Ac3Parser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + return Vec::new(); + } + + let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0); + + // AC3: each PES = one frame, always a keyframe + vec![Frame { + pts_ns, + keyframe: true, + data: pes.data.clone(), + }] + } + + fn codec_private(&self) -> Option> { + // AC3 doesn't need codecPrivate in MKV + None + } +} diff --git a/src/mux/codec/dts.rs b/src/mux/codec/dts.rs new file mode 100644 index 0000000..3b4e40b --- /dev/null +++ b/src/mux/codec/dts.rs @@ -0,0 +1,26 @@ +//! DTS / DTS-HD elementary stream parser. +//! +//! DTS core syncword: 0x7FFE8001 (32 bits). +//! DTS-HD MA/HRA extension follows the core frame. +//! All frames are keyframes (no inter-frame dependencies). +//! Each PES packet = one frame. + +use super::{CodecParser, Frame, PesPacket, pts_to_ns}; + +pub struct DtsParser; + +impl DtsParser { + pub fn new() -> Self { Self } +} + +impl CodecParser for DtsParser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + 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() }] + } + + fn codec_private(&self) -> Option> { None } +} diff --git a/src/mux/codec/h264.rs b/src/mux/codec/h264.rs new file mode 100644 index 0000000..ad57c32 --- /dev/null +++ b/src/mux/codec/h264.rs @@ -0,0 +1,192 @@ +//! H.264 (AVC) elementary stream parser. +//! +//! Extracts SPS and PPS NAL units for MKV codecPrivate. +//! Detects keyframes (IDR slices). +//! Each PES packet = one access unit = one frame. + +use super::{CodecParser, Frame, PesPacket, pts_to_ns}; + +/// H.264 NAL unit types we care about. +const NAL_SLICE: u8 = 1; +const NAL_SLICE_IDR: u8 = 5; +const NAL_SEI: u8 = 6; +const NAL_SPS: u8 = 7; +const NAL_PPS: u8 = 8; +const NAL_AUD: u8 = 9; + +pub struct H264Parser { + sps: Option>, + pps: Option>, +} + +impl H264Parser { + pub fn new() -> Self { + Self { sps: None, pps: None } + } +} + +impl CodecParser for H264Parser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + return Vec::new(); + } + + let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0); + + // Scan NAL units for SPS, PPS, and IDR detection + let mut keyframe = false; + let mut frame_data = Vec::new(); + + for nal in NalIterator::new(&pes.data) { + let nal_type = nal[0] & 0x1F; + + match nal_type { + NAL_SPS => { + self.sps = Some(nal.to_vec()); + } + NAL_PPS => { + self.pps = Some(nal.to_vec()); + } + NAL_SLICE_IDR => { + keyframe = true; + } + _ => {} + } + } + + // Convert Annex B (start code prefixed) to length-prefixed NALUs. + // MKV with AVCDecoderConfigurationRecord expects 4-byte length prefix per NALU. + // Skip SPS/PPS/AUD NALUs — they're in codecPrivate, not in frame data. + for nal in NalIterator::new(&pes.data) { + let nal_type = nal[0] & 0x1F; + // Skip parameter sets and access unit delimiters + if nal_type == NAL_SPS || nal_type == NAL_PPS || nal_type == NAL_AUD { + continue; + } + // 4-byte big-endian length prefix + let len = nal.len() as u32; + frame_data.extend_from_slice(&len.to_be_bytes()); + frame_data.extend_from_slice(nal); + } + + if frame_data.is_empty() { + return Vec::new(); + } + + vec![Frame { + pts_ns, + keyframe, + data: frame_data, + }] + } + + fn codec_private(&self) -> Option> { + // Build AVCDecoderConfigurationRecord from SPS + PPS + let sps = self.sps.as_ref()?; + let pps = self.pps.as_ref()?; + + // AVCDecoderConfigurationRecord (ISO 14496-15): + // configurationVersion = 1 + // AVCProfileIndication = SPS[1] + // profile_compatibility = SPS[2] + // AVCLevelIndication = SPS[3] + // lengthSizeMinusOne = 3 (4-byte length prefix) + // numOfSequenceParameterSets = 1 + // sequenceParameterSetLength = sps.len() + // sequenceParameterSetNALUnit = sps + // numOfPictureParameterSets = 1 + // pictureParameterSetLength = pps.len() + // pictureParameterSetNALUnit = pps + + let mut record = Vec::new(); + record.push(1); // configurationVersion + record.push(sps[1]); // profile + record.push(sps[2]); // compatibility + record.push(sps[3]); // level + record.push(0xFF); // 6 bits reserved (111111) + 2 bits lengthSizeMinusOne (11 = 3) + record.push(0xE1); // 3 bits reserved (111) + 5 bits numSPS (1) + record.push((sps.len() >> 8) as u8); + record.push(sps.len() as u8); + record.extend_from_slice(sps); + record.push(1); // numPPS + record.push((pps.len() >> 8) as u8); + record.push(pps.len() as u8); + record.extend_from_slice(pps); + + Some(record) + } +} + +/// Iterator over NAL units in Annex B byte stream. +/// Finds start codes (00 00 01 or 00 00 00 01) and yields the data between them. +struct NalIterator<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> NalIterator<'a> { + fn new(data: &'a [u8]) -> Self { + // Skip to first start code + let pos = find_start_code(data, 0).unwrap_or(data.len()); + Self { data, pos } + } +} + +impl<'a> Iterator for NalIterator<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option<&'a [u8]> { + if self.pos >= self.data.len() { + return None; + } + + // Skip the start code at current position + let nal_start = skip_start_code(self.data, self.pos)?; + + // Find next start code (or end of data) + let nal_end = find_start_code(self.data, nal_start).unwrap_or(self.data.len()); + + // Remove trailing zeros (part of next start code's zero prefix) + let mut end = nal_end; + while end > nal_start && self.data[end - 1] == 0x00 { + end -= 1; + } + + self.pos = nal_end; + + if end > nal_start { + Some(&self.data[nal_start..end]) + } else { + self.next() + } + } +} + +/// Find the position of the next start code (00 00 01) at or after `from`. +pub fn find_start_code(data: &[u8], from: usize) -> Option { + 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 +} + +/// Skip past the start code at position `pos`, returning the first byte after it. +pub fn skip_start_code(data: &[u8], pos: usize) -> Option { + if pos + 2 >= data.len() { + return None; + } + if data[pos] == 0x00 && data[pos + 1] == 0x00 { + if pos + 3 < data.len() && data[pos + 2] == 0x00 && data[pos + 3] == 0x01 { + return Some(pos + 4); // 4-byte start code + } + if data[pos + 2] == 0x01 { + return Some(pos + 3); // 3-byte start code + } + } + None +} diff --git a/src/mux/codec/hevc.rs b/src/mux/codec/hevc.rs new file mode 100644 index 0000000..738da9b --- /dev/null +++ b/src/mux/codec/hevc.rs @@ -0,0 +1,170 @@ +//! HEVC (H.265) elementary stream parser. +//! +//! Extracts VPS, SPS, PPS NAL units for MKV codecPrivate. +//! 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}; + +// HEVC NAL unit types +const NAL_VPS: u8 = 32; +const NAL_SPS: u8 = 33; +const NAL_PPS: u8 = 34; +const NAL_AUD: u8 = 35; +// IRAP types (keyframes): BLA, IDR, CRA +const NAL_BLA_W_LP: u8 = 16; +const NAL_RSV_IRAP_VCL23: u8 = 23; + +pub struct HevcParser { + vps: Option>, + sps: Option>, + pps: Option>, +} + +impl HevcParser { + pub fn new() -> Self { + Self { vps: None, sps: None, pps: None } + } +} + +impl CodecParser for HevcParser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + return Vec::new(); + } + + let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0); + let data = &pes.data; + let mut keyframe = false; + + // Scan NAL units + let mut pos = 0; + while let Some(sc_pos) = find_start_code(data, pos) { + 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; } + + if nal_start < data.len() { + // HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte. + let nal_type = (data[nal_start] >> 1) & 0x3F; + + match nal_type { + NAL_VPS => self.vps = Some(data[nal_start..end].to_vec()), + NAL_SPS => self.sps = Some(data[nal_start..end].to_vec()), + NAL_PPS => self.pps = Some(data[nal_start..end].to_vec()), + t if t >= NAL_BLA_W_LP && t <= NAL_RSV_IRAP_VCL23 => { + keyframe = true; + } + _ => {} + } + } + pos = next; + } else { + break; + } + } + + // Convert Annex B to length-prefixed NALUs. + // Skip VPS/SPS/PPS/AUD — they're in codecPrivate. + let mut frame_data = Vec::new(); + let mut pos = 0; + while let Some(sc_pos) = find_start_code(&pes.data, pos) { + 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; } + + 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 { + let nal = &pes.data[nal_start..end]; + let len = nal.len() as u32; + frame_data.extend_from_slice(&len.to_be_bytes()); + frame_data.extend_from_slice(nal); + } + } + pos = next; + } else { + break; + } + } + + if frame_data.is_empty() { + return Vec::new(); + } + + vec![Frame { + pts_ns, + keyframe, + data: frame_data, + }] + } + + fn codec_private(&self) -> Option> { + // HEVCDecoderConfigurationRecord (ISO 14496-15) + let vps = self.vps.as_ref()?; + let sps = self.sps.as_ref()?; + let pps = self.pps.as_ref()?; + + // Simplified: store as arrays in Annex B format + // Full HEVCDecoderConfigurationRecord is complex — for now, concatenate + let mut record = Vec::new(); + + // Minimal HEVCDecoderConfigurationRecord header + 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 { + record.push(0); + } + // general_profile_compatibility_flags (4 bytes) + record.extend_from_slice(&[0, 0, 0, 0]); + // general_constraint_indicator_flags (6 bytes) + record.extend_from_slice(&[0, 0, 0, 0, 0, 0]); + // general_level_idc + record.push(if sps.len() > 12 { sps[12] } else { 0 }); + // min_spatial_segmentation_idc (4 + 12 bits) + record.extend_from_slice(&[0xF0, 0x00]); + // parallelismType (6 + 2 bits) + record.push(0xFC); + // chromaFormat (6 + 2 bits) + record.push(0xFC | 1); // 4:2:0 + // bitDepthLumaMinus8 (5 + 3 bits) + record.push(0xF8); + // bitDepthChromaMinus8 (5 + 3 bits) + record.push(0xF8); + // avgFrameRate + record.extend_from_slice(&[0, 0]); + // constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne + record.push(0x03); // lengthSizeMinusOne = 3 (4 bytes) + // numOfArrays + record.push(3); // VPS, SPS, PPS + + // VPS array + record.push(0x20 | (NAL_VPS & 0x3F)); // array_completeness + NAL type + record.extend_from_slice(&[0, 1]); // numNalus = 1 + record.push((vps.len() >> 8) as u8); + record.push(vps.len() as u8); + record.extend_from_slice(vps); + + // SPS array + record.push(0x20 | (NAL_SPS & 0x3F)); + record.extend_from_slice(&[0, 1]); + record.push((sps.len() >> 8) as u8); + record.push(sps.len() as u8); + record.extend_from_slice(sps); + + // PPS array + record.push(0x20 | (NAL_PPS & 0x3F)); + record.extend_from_slice(&[0, 1]); + record.push((pps.len() >> 8) as u8); + record.push(pps.len() as u8); + record.extend_from_slice(pps); + + Some(record) + } +} diff --git a/src/mux/codec/mod.rs b/src/mux/codec/mod.rs new file mode 100644 index 0000000..15357d7 --- /dev/null +++ b/src/mux/codec/mod.rs @@ -0,0 +1,88 @@ +//! Elementary stream codec parsers. +//! +//! Each parser takes PES packets and produces frames suitable for MKV muxing. +//! Responsibilities: +//! - Find frame boundaries +//! - Extract codec initialization data (SPS/PPS, etc.) +//! - Determine keyframe status +//! - Convert PTS from 90kHz to nanoseconds + +pub mod ac3; +pub mod h264; +pub mod hevc; +pub mod vc1; +pub mod dts; +pub mod truehd; +pub mod pgs; + +use crate::disc::Codec; +use super::ts::PesPacket; + +/// A single frame ready for MKV muxing. +pub struct Frame { + /// Presentation timestamp in nanoseconds. + pub pts_ns: i64, + /// Whether this is a keyframe (used for cue points). + pub keyframe: bool, + /// Frame data (elementary stream bytes). + pub data: Vec, +} + +/// Convert 90kHz PTS to nanoseconds. +pub fn pts_to_ns(pts: i64) -> i64 { + pts * 100_000 / 9 +} + +/// Trait for codec-specific elementary stream parsers. +pub trait CodecParser: Send { + /// Parse a PES packet into zero or more frames. + /// Most codecs: one PES = one frame. + /// Some (TrueHD): multiple access units per PES. + fn parse(&mut self, pes: &PesPacket) -> Vec; + + /// Get codec initialization data (e.g., SPS+PPS for H.264). + /// Returns None until enough data has been seen. + fn codec_private(&self) -> Option>; +} + +/// Passthrough parser — treats each PES as one frame, no parsing. +/// Used for codecs where PES = frame (AC3, DTS, PGS). +pub struct PassthroughParser { + keyframe: bool, +} + +impl PassthroughParser { + pub fn new(always_keyframe: bool) -> Self { + Self { keyframe: always_keyframe } + } +} + +impl CodecParser for PassthroughParser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0); + vec![Frame { + pts_ns, + keyframe: self.keyframe, + data: pes.data.clone(), + }] + } + + fn codec_private(&self) -> Option> { + None + } +} + +/// Create the appropriate parser for a codec. +pub fn parser_for_codec(codec: Codec) -> Box { + match codec { + Codec::H264 => Box::new(h264::H264Parser::new()), + Codec::Hevc => Box::new(hevc::HevcParser::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()), + Codec::TrueHd => Box::new(truehd::TrueHdParser::new()), + Codec::Pgs => Box::new(pgs::PgsParser::new()), + Codec::Lpcm => Box::new(PassthroughParser::new(true)), + _ => Box::new(PassthroughParser::new(true)), + } +} diff --git a/src/mux/codec/pgs.rs b/src/mux/codec/pgs.rs new file mode 100644 index 0000000..9da6b7a --- /dev/null +++ b/src/mux/codec/pgs.rs @@ -0,0 +1,25 @@ +//! HDMV PGS (Presentation Graphics Stream) subtitle parser. +//! +//! PGS segments: PCS, WDS, PDS, ODS, END. +//! Each PES packet contains one or more segments. +//! All segments are keyframes (no inter-segment dependencies). + +use super::{CodecParser, Frame, PesPacket, pts_to_ns}; + +pub struct PgsParser; + +impl PgsParser { + pub fn new() -> Self { Self } +} + +impl CodecParser for PgsParser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + 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() }] + } + + fn codec_private(&self) -> Option> { None } +} diff --git a/src/mux/codec/truehd.rs b/src/mux/codec/truehd.rs new file mode 100644 index 0000000..fe34443 --- /dev/null +++ b/src/mux/codec/truehd.rs @@ -0,0 +1,27 @@ +//! Dolby TrueHD / Atmos elementary stream parser. +//! +//! TrueHD major sync: 0xF8726FBA at a 4-byte aligned position. +//! Access units consist of a major sync followed by minor syncs. +//! An embedded AC3 core is in substream 0 for backward compatibility. +//! All access units are keyframes. +//! Each PES packet = one access unit. + +use super::{CodecParser, Frame, PesPacket, pts_to_ns}; + +pub struct TrueHdParser; + +impl TrueHdParser { + pub fn new() -> Self { Self } +} + +impl CodecParser for TrueHdParser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + 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() }] + } + + fn codec_private(&self) -> Option> { None } +} diff --git a/src/mux/codec/vc1.rs b/src/mux/codec/vc1.rs new file mode 100644 index 0000000..db75e2d --- /dev/null +++ b/src/mux/codec/vc1.rs @@ -0,0 +1,115 @@ +//! VC-1 (SMPTE 421M) elementary stream parser. +//! +//! VC-1 uses start codes similar to MPEG-2. +//! Sequence header (0x0F) contains codec initialization data. +//! Frame start = Frame header start code (0x0D). +//! I-frames (keyframes) are identified from the frame header. + +use super::{CodecParser, Frame, PesPacket, pts_to_ns}; + +const SC_SEQUENCE_HEADER: u8 = 0x0F; +const SC_ENTRY_POINT: u8 = 0x0E; +const SC_FRAME: u8 = 0x0D; + +pub struct Vc1Parser { + seq_header: Option>, + entry_point: Option>, +} + +impl Vc1Parser { + pub fn new() -> Self { + Self { seq_header: None, entry_point: None } + } +} + +impl CodecParser for Vc1Parser { + fn parse(&mut self, pes: &PesPacket) -> Vec { + if pes.data.is_empty() { + return Vec::new(); + } + + let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0); + let mut keyframe = false; + + // Scan for start codes (00 00 01 XX) + let data = &pes.data; + let mut i = 0; + while i + 3 < data.len() { + if data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01 { + let sc_type = data[i + 3]; + match sc_type { + SC_SEQUENCE_HEADER => { + // Capture everything from here to next start code + let end = find_next_sc(data, i + 4).unwrap_or(data.len()); + self.seq_header = Some(data[i..end].to_vec()); + } + SC_ENTRY_POINT => { + let end = find_next_sc(data, i + 4).unwrap_or(data.len()); + self.entry_point = Some(data[i..end].to_vec()); + } + SC_FRAME => { + // First frame after sequence header + entry point is a keyframe + if self.seq_header.is_some() && self.entry_point.is_some() { + keyframe = true; + } + // Also check frame type from bitstream (bit after start code) + if i + 4 < data.len() { + // For Advanced profile: first 2 bits of frame data indicate type + // But simpler: any frame preceded by seq+entry is I-frame + } + } + _ => {} + } + i += 4; + } else { + i += 1; + } + } + + vec![Frame { + pts_ns, + keyframe, + data: pes.data.clone(), + }] + } + + fn codec_private(&self) -> Option> { + // MKV V_MS/VFW/FOURCC requires BITMAPINFOHEADER (40 bytes) + extra codec data. + // The sequence header + entry point go as extra data after the header. + let sh = self.seq_header.as_ref()?; + let ep = self.entry_point.as_ref()?; + + let extra_len = sh.len() + ep.len(); + let header_size: u32 = 40 + extra_len as u32; + + 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 + + // Extra codec data: sequence header + entry point (Annex B) + cp.extend_from_slice(sh); + cp.extend_from_slice(ep); + + Some(cp) + } +} + +fn find_next_sc(data: &[u8], from: usize) -> Option { + for i in from..data.len().saturating_sub(2) { + if data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01 { + return Some(i); + } + } + None +} diff --git a/src/mux/ebml.rs b/src/mux/ebml.rs new file mode 100644 index 0000000..5ada5ca --- /dev/null +++ b/src/mux/ebml.rs @@ -0,0 +1,272 @@ +//! EBML (Extensible Binary Meta Language) write primitives for Matroska. +//! +//! EBML uses variable-length integers for element IDs and sizes. +//! This module provides low-level writers for constructing MKV files. + +use std::io::{self, Write, Seek, SeekFrom}; + +/// Write an EBML element ID (1-4 bytes, already encoded). +/// Element IDs are predefined constants — we write them verbatim. +pub fn write_id(w: &mut impl Write, id: u32) -> io::Result<()> { + if id <= 0x7F { + w.write_all(&[id as u8]) + } else if id <= 0x7FFF { + w.write_all(&[(id >> 8) as u8, id as u8]) + } else if id <= 0x7F_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]) + } +} + +/// Write an EBML variable-length size (1-8 bytes). +/// Uses the EBML VINT encoding: leading bits indicate width. +pub fn write_size(w: &mut impl Write, size: u64) -> io::Result<()> { + if size < 0x7F { + w.write_all(&[(size as u8) | 0x80]) + } 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, + ]) + } else if size < 0x0FFF_FFFF { + w.write_all(&[ + ((size >> 24) as u8) | 0x10, + (size >> 16) as u8, + (size >> 8) as u8, + size as u8, + ]) + } else { + // 8-byte size for large elements + w.write_all(&[ + 0x01, + (size >> 48) as u8, + (size >> 40) as u8, + (size >> 32) as u8, + (size >> 24) as u8, + (size >> 16) as u8, + (size >> 8) as u8, + size as u8, + ]) + } +} + +/// Write an EBML "unknown size" marker (all 1s in VINT, 8 bytes). +/// Used for the Segment element when total size isn't known upfront. +pub fn write_unknown_size(w: &mut impl Write) -> io::Result<()> { + w.write_all(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) +} + +/// Write a complete EBML unsigned integer element. +pub fn write_uint(w: &mut impl Write, id: u32, val: u64) -> io::Result<()> { + write_id(w, id)?; + if val <= 0xFF { + write_size(w, 1)?; + w.write_all(&[val as u8]) + } else if val <= 0xFFFF { + write_size(w, 2)?; + w.write_all(&[(val >> 8) as u8, val as u8]) + } else if val <= 0xFF_FFFF { + write_size(w, 3)?; + w.write_all(&[(val >> 16) as u8, (val >> 8) as u8, val as u8]) + } 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, + ]) + } else { + write_size(w, 8)?; + w.write_all(&val.to_be_bytes()) + } +} + +/// Write a complete EBML signed integer element. +pub fn write_int(w: &mut impl Write, id: u32, val: i64) -> io::Result<()> { + write_uint(w, id, val as u64) +} + +/// Write a complete EBML float element (8-byte double). +pub fn write_float(w: &mut impl Write, id: u32, val: f64) -> io::Result<()> { + write_id(w, id)?; + write_size(w, 8)?; + w.write_all(&val.to_be_bytes()) +} + +/// Write a complete EBML UTF-8 string element. +pub fn write_string(w: &mut impl Write, id: u32, val: &str) -> io::Result<()> { + write_id(w, id)?; + write_size(w, val.len() as u64)?; + w.write_all(val.as_bytes()) +} + +/// Write a complete EBML binary element. +pub fn write_binary(w: &mut impl Write, id: u32, data: &[u8]) -> io::Result<()> { + write_id(w, id)?; + write_size(w, data.len() as u64)?; + w.write_all(data) +} + +/// Start a master element: write ID + placeholder size. +/// Returns the file offset of the size field for later fixup. +pub fn start_master(w: &mut W, id: u32) -> io::Result { + write_id(w, id)?; + let size_pos = w.stream_position()?; + // 8-byte size placeholder (will be overwritten by end_master) + w.write_all(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])?; + Ok(size_pos) +} + +/// End a master element: seek back and write the actual size. +pub fn end_master(w: &mut W, size_pos: u64) -> io::Result<()> { + let end_pos = w.stream_position()?; + let data_size = end_pos - size_pos - 8; // subtract the 8-byte size field itself + w.seek(SeekFrom::Start(size_pos))?; + // Write as 8-byte VINT: 0x01 followed by 7 bytes of size + w.write_all(&[ + 0x01, + (data_size >> 48) as u8, + (data_size >> 40) as u8, + (data_size >> 32) as u8, + (data_size >> 24) as u8, + (data_size >> 16) as u8, + (data_size >> 8) as u8, + data_size as u8, + ])?; + w.seek(SeekFrom::Start(end_pos))?; + Ok(()) +} + +// ============================================================ +// Matroska Element IDs +// ============================================================ + +// EBML Header +pub const EBML: u32 = 0x1A45DFA3; +pub const EBML_VERSION: u32 = 0x4286; +pub const EBML_READ_VERSION: u32 = 0x42F7; +pub const EBML_MAX_ID_LENGTH: u32 = 0x42F2; +pub const EBML_MAX_SIZE_LENGTH: u32 = 0x42F3; +pub const EBML_DOC_TYPE: u32 = 0x4282; +pub const EBML_DOC_TYPE_VERSION: u32 = 0x4287; +pub const EBML_DOC_TYPE_READ_VERSION: u32 = 0x4285; + +// Segment +pub const SEGMENT: u32 = 0x18538067; + +// Seek Head +pub const SEEK_HEAD: u32 = 0x114D9B74; +pub const SEEK: u32 = 0x4DBB; +pub const SEEK_ID: u32 = 0x53AB; +pub const SEEK_POSITION: u32 = 0x53AC; + +// Segment Info +pub const INFO: u32 = 0x1549A966; +pub const TIMESTAMP_SCALE: u32 = 0x2AD7B1; +pub const DURATION: u32 = 0x4489; +pub const MUXING_APP: u32 = 0x4D80; +pub const WRITING_APP: u32 = 0x5741; +pub const TITLE: u32 = 0x7BA9; + +// Tracks +pub const TRACKS: u32 = 0x1654AE6B; +pub const TRACK_ENTRY: u32 = 0xAE; +pub const TRACK_NUMBER: u32 = 0xD7; +pub const TRACK_UID: u32 = 0x73C5; +pub const TRACK_TYPE: u32 = 0x83; +pub const FLAG_LACING: u32 = 0x9C; +pub const FLAG_DEFAULT: u32 = 0x88; +pub const FLAG_FORCED: u32 = 0x55AA; +pub const LANGUAGE: u32 = 0x22B59C; +pub const CODEC_ID: u32 = 0x86; +pub const CODEC_PRIVATE: u32 = 0x63A2; +pub const DEFAULT_DURATION: u32 = 0x23E383; + +// Video +pub const VIDEO: u32 = 0xE0; +pub const PIXEL_WIDTH: u32 = 0xB0; +pub const PIXEL_HEIGHT: u32 = 0xBA; +pub const DISPLAY_WIDTH: u32 = 0x54B0; +pub const DISPLAY_HEIGHT: u32 = 0x54BA; +pub const COLOUR: u32 = 0x55B0; +pub const TRANSFER_CHARACTERISTICS: u32 = 0x55BA; +pub const MATRIX_COEFFICIENTS: u32 = 0x55B1; +pub const PRIMARIES: u32 = 0x55BB; +pub const RANGE: u32 = 0x55B9; + +// Audio +pub const AUDIO: u32 = 0xE1; +pub const SAMPLING_FREQUENCY: u32 = 0xB5; +pub const CHANNELS: u32 = 0x9F; +pub const BIT_DEPTH: u32 = 0x6264; + +// Cluster +pub const CLUSTER: u32 = 0x1F43B675; +pub const CLUSTER_TIMESTAMP: u32 = 0xE7; +pub const SIMPLE_BLOCK: u32 = 0xA3; + +// Cues +pub const CUES: u32 = 0x1C53BB6B; +pub const CUE_POINT: u32 = 0xBB; +pub const CUE_TIME: u32 = 0xB3; +pub const CUE_TRACK_POSITIONS: u32 = 0xB7; +pub const CUE_TRACK: u32 = 0xF7; +pub const CUE_CLUSTER_POSITION: u32 = 0xF1; + +// Track types +pub const TRACK_TYPE_VIDEO: u64 = 1; +pub const TRACK_TYPE_AUDIO: u64 = 2; +pub const TRACK_TYPE_SUBTITLE: u64 = 17; + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_write_size() { + let mut buf = Vec::new(); + write_size(&mut buf, 0).unwrap(); + assert_eq!(buf, [0x80]); + + buf.clear(); + write_size(&mut buf, 127).unwrap(); + assert_eq!(buf, [0xC0, 127]); // 127 >= 0x7F, uses 2 bytes + + buf.clear(); + write_size(&mut buf, 126).unwrap(); + assert_eq!(buf, [126 | 0x80]); // 126 < 0x7F, uses 1 byte + } + + #[test] + 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 + assert_eq!(buf, [0x42, 0x86, 0x81, 0x01]); + } + + #[test] + fn test_write_string() { + let mut buf = Vec::new(); + write_string(&mut buf, 0x4282, "matroska").unwrap(); + // ID: 42 82, Size: 88 (8 bytes), Data: "matroska" + assert_eq!(&buf[0..2], &[0x42, 0x82]); + assert_eq!(buf[2], 0x88); // size = 8 + assert_eq!(&buf[3..], b"matroska"); + } + + #[test] + fn test_master_element() { + let mut buf = Cursor::new(Vec::new()); + let pos = start_master(&mut buf, EBML).unwrap(); + write_uint(&mut buf, EBML_VERSION, 1).unwrap(); + end_master(&mut buf, pos).unwrap(); + let data = buf.into_inner(); + // EBML header: 1A 45 DF A3, then 8-byte size, then content + assert_eq!(&data[0..4], &[0x1A, 0x45, 0xDF, 0xA3]); + } +} diff --git a/src/mux/lookahead.rs b/src/mux/lookahead.rs new file mode 100644 index 0000000..d3fc84e --- /dev/null +++ b/src/mux/lookahead.rs @@ -0,0 +1,127 @@ +//! LookaheadBuffer — generic pre-scan buffer for stream pipelines. +//! +//! Accumulates data up to a configurable limit. When the consumer finds +//! what it needs, the buffer can be drained (fast path, no re-read). +//! If the buffer fills before the consumer is satisfied, it signals +//! overflow — the caller should discard and re-read from the source. +//! +//! Used by MkvStream to collect SPS/PPS before writing the MKV header. +//! Reusable for any stream stage that needs to look ahead. + +/// Default lookahead buffer size: 5 MB. +pub const DEFAULT_LOOKAHEAD_SIZE: usize = 5 * 1024 * 1024; + +/// Lookahead buffer states. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LookaheadState { + /// Still collecting data, haven't found what we need yet. + Collecting, + /// Found what we need, buffer has the data ready to drain. + Ready, + /// Buffer overflowed before finding what we need. + /// Caller should discard buffer, finish scanning without buffering, + /// then re-read from the source. + Overflow, +} + +/// A bounded lookahead buffer. +pub struct LookaheadBuffer { + data: Vec, + max_size: usize, + state: LookaheadState, +} + +impl LookaheadBuffer { + /// Create a new buffer with the given max size. + /// Pass 0 for no buffering (always overflows immediately). + pub fn new(max_size: usize) -> Self { + Self { + data: Vec::with_capacity(max_size.min(DEFAULT_LOOKAHEAD_SIZE)), + max_size, + state: LookaheadState::Collecting, + } + } + + /// Push data into the buffer. Returns the new state. + /// If the buffer would overflow, transitions to Overflow state. + pub fn push(&mut self, chunk: &[u8]) -> LookaheadState { + if self.state != LookaheadState::Collecting { + return self.state; + } + + if self.data.len() + chunk.len() > self.max_size { + self.state = LookaheadState::Overflow; + return self.state; + } + + self.data.extend_from_slice(chunk); + self.state + } + + /// Mark the buffer as ready — we found what we need. + pub fn mark_ready(&mut self) { + if self.state == LookaheadState::Collecting { + self.state = LookaheadState::Ready; + } + } + + /// Get the buffered data (only valid in Ready state). + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Take ownership of the buffered data, clearing the buffer. + pub fn drain(&mut self) -> Vec { + self.state = LookaheadState::Collecting; + std::mem::take(&mut self.data) + } + + /// Current state. + pub fn state(&self) -> LookaheadState { + self.state + } + + /// How many bytes are buffered. + pub fn len(&self) -> usize { + self.data.len() + } + + /// Is the buffer empty? + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Max size this buffer can hold. + pub fn max_size(&self) -> usize { + self.max_size + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_flow() { + let mut buf = LookaheadBuffer::new(100); + assert_eq!(buf.push(b"hello"), LookaheadState::Collecting); + assert_eq!(buf.push(b"world"), LookaheadState::Collecting); + assert_eq!(buf.len(), 10); + buf.mark_ready(); + assert_eq!(buf.state(), LookaheadState::Ready); + assert_eq!(buf.data(), b"helloworld"); + } + + #[test] + fn test_overflow() { + let mut buf = LookaheadBuffer::new(5); + assert_eq!(buf.push(b"abc"), LookaheadState::Collecting); + assert_eq!(buf.push(b"def"), LookaheadState::Overflow); + } + + #[test] + fn test_zero_size() { + let mut buf = LookaheadBuffer::new(0); + assert_eq!(buf.push(b"a"), LookaheadState::Overflow); + } +} diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs new file mode 100644 index 0000000..ad50627 --- /dev/null +++ b/src/mux/mkv.rs @@ -0,0 +1,375 @@ +//! Matroska (MKV) muxer. +//! +//! Writes EBML header, Segment with tracks, clusters, and cues. +//! 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}; + +/// MKV track definition (built from disc stream metadata). +pub struct MkvTrack { + pub track_type: u64, // 1=video, 2=audio, 17=subtitle + pub codec_id: &'static str, + pub language: String, + pub codec_private: Option>, + pub is_default: bool, + pub is_forced: bool, + // Video-specific + pub pixel_width: u32, + pub pixel_height: u32, + // Audio-specific + pub sample_rate: f64, + pub channels: u8, + pub bit_depth: u8, +} + +impl MkvTrack { + pub fn video(v: &VideoStream) -> Self { + let codec_id = match v.codec { + Codec::H264 => "V_MPEG4/ISO/AVC", + Codec::Hevc => "V_MPEGH/ISO/HEVC", + Codec::Vc1 => "V_MS/VFW/FOURCC", + Codec::Mpeg2 => "V_MPEG2", + _ => "V_MPEG2", + }; + let (w, h) = parse_resolution(&v.resolution); + Self { + track_type: ebml::TRACK_TYPE_VIDEO, + codec_id, + language: "und".into(), + codec_private: None, // filled later by parser + is_default: !v.secondary, + is_forced: false, + pixel_width: w, + pixel_height: h, + sample_rate: 0.0, + channels: 0, + bit_depth: 0, + } + } + + pub fn audio(a: &AudioStream) -> Self { + let codec_id = match a.codec { + Codec::Ac3 => "A_AC3", + Codec::Ac3Plus => "A_EAC3", + Codec::TrueHd => "A_TRUEHD", + Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => "A_DTS", + Codec::Lpcm => "A_PCM/INT/BIG", + _ => "A_AC3", + }; + let sr = parse_sample_rate(&a.sample_rate); + let ch = parse_channels(&a.channels); + Self { + track_type: ebml::TRACK_TYPE_AUDIO, + codec_id, + language: a.language.clone(), + codec_private: None, + is_default: !a.secondary, + is_forced: false, + pixel_width: 0, + pixel_height: 0, + sample_rate: sr, + channels: ch, + bit_depth: 0, + } + } + + pub fn subtitle(s: &SubtitleStream) -> Self { + Self { + track_type: ebml::TRACK_TYPE_SUBTITLE, + codec_id: "S_HDMV/PGS", + language: s.language.clone(), + codec_private: None, + is_default: false, + is_forced: s.forced, + pixel_width: 0, + pixel_height: 0, + sample_rate: 0.0, + channels: 0, + bit_depth: 0, + } + } +} + +/// Cue point for seeking. +struct CuePoint { + timestamp_ms: i64, + track: usize, + cluster_pos: u64, // relative to Segment start +} + +/// MKV muxer. Call write_frame() for each frame, then finish() at the end. +pub struct MkvMuxer { + writer: W, + segment_start: u64, + cluster_open: bool, + cluster_pos: u64, + cluster_size_pos: u64, + cluster_ts_ms: i64, + cues: Vec, + frame_count: u64, + /// File positions of codecPrivate placeholders (track_idx → offset, max_size). + /// Used to seek back and fill in SPS/PPS after first keyframe. + codec_private_slots: Vec>, + codec_private_filled: Vec, +} + +/// New cluster every 5 seconds. +const CLUSTER_DURATION_MS: i64 = 5000; + +impl MkvMuxer { + /// 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 { + // EBML Header + let ebml_pos = ebml::start_master(&mut writer, ebml::EBML)?; + ebml::write_uint(&mut writer, ebml::EBML_VERSION, 1)?; + ebml::write_uint(&mut writer, ebml::EBML_READ_VERSION, 1)?; + ebml::write_uint(&mut writer, ebml::EBML_MAX_ID_LENGTH, 4)?; + ebml::write_uint(&mut writer, ebml::EBML_MAX_SIZE_LENGTH, 8)?; + ebml::write_string(&mut writer, ebml::EBML_DOC_TYPE, "matroska")?; + ebml::write_uint(&mut writer, ebml::EBML_DOC_TYPE_VERSION, 4)?; + ebml::write_uint(&mut writer, ebml::EBML_DOC_TYPE_READ_VERSION, 2)?; + ebml::end_master(&mut writer, ebml_pos)?; + + // Segment (unknown size — we'll write cues at the end) + ebml::write_id(&mut writer, ebml::SEGMENT)?; + ebml::write_unknown_size(&mut writer)?; + let segment_start = writer.stream_position()?; + + // Info + 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_string(&mut writer, ebml::MUXING_APP, "freemkv")?; + ebml::write_string(&mut writer, ebml::WRITING_APP, "freemkv")?; + if let Some(t) = title { + ebml::write_string(&mut writer, ebml::TITLE, t)?; + } + ebml::end_master(&mut writer, info_pos)?; + + // Tracks + let mut codec_private_slots: Vec> = Vec::new(); + let mut codec_private_filled: Vec = Vec::new(); + let tracks_pos = ebml::start_master(&mut writer, ebml::TRACKS)?; + for (i, track) in tracks.iter().enumerate() { + let entry_pos = ebml::start_master(&mut writer, ebml::TRACK_ENTRY)?; + ebml::write_uint(&mut writer, ebml::TRACK_NUMBER, (i + 1) as u64)?; + ebml::write_uint(&mut writer, ebml::TRACK_UID, (i + 1) as u64)?; + ebml::write_uint(&mut writer, ebml::TRACK_TYPE, track.track_type)?; + ebml::write_uint(&mut writer, ebml::FLAG_LACING, 0)?; + ebml::write_string(&mut writer, ebml::CODEC_ID, track.codec_id)?; + ebml::write_string(&mut writer, ebml::LANGUAGE, &track.language)?; + + if !track.is_default { + ebml::write_uint(&mut writer, ebml::FLAG_DEFAULT, 0)?; + } + if track.is_forced { + ebml::write_uint(&mut writer, ebml::FLAG_FORCED, 1)?; + } + + if let Some(ref cp) = track.codec_private { + ebml::write_binary(&mut writer, ebml::CODEC_PRIVATE, cp)?; + codec_private_slots.push(None); // already filled + codec_private_filled.push(true); + } else if track.track_type == ebml::TRACK_TYPE_VIDEO { + // Reserve space for codecPrivate — will be filled after first keyframe + // Reserve 256 bytes (enough for SPS+PPS or VPS+SPS+PPS) + let cp_pos = writer.stream_position()?; + let placeholder = vec![0u8; 256]; + ebml::write_binary(&mut writer, ebml::CODEC_PRIVATE, &placeholder)?; + codec_private_slots.push(Some((cp_pos, 256))); + codec_private_filled.push(false); + } else { + codec_private_slots.push(None); + codec_private_filled.push(true); + } + + // Video-specific + if track.track_type == ebml::TRACK_TYPE_VIDEO && track.pixel_width > 0 { + let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?; + ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?; + ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?; + ebml::end_master(&mut writer, vid_pos)?; + } + + // Audio-specific + if track.track_type == ebml::TRACK_TYPE_AUDIO && track.sample_rate > 0.0 { + let aud_pos = ebml::start_master(&mut writer, ebml::AUDIO)?; + ebml::write_float(&mut writer, ebml::SAMPLING_FREQUENCY, track.sample_rate)?; + ebml::write_uint(&mut writer, ebml::CHANNELS, track.channels as u64)?; + if track.bit_depth > 0 { + ebml::write_uint(&mut writer, ebml::BIT_DEPTH, track.bit_depth as u64)?; + } + ebml::end_master(&mut writer, aud_pos)?; + } + + ebml::end_master(&mut writer, entry_pos)?; + } + ebml::end_master(&mut writer, tracks_pos)?; + + Ok(Self { + writer, + segment_start, + cluster_open: false, + cluster_pos: 0, + cluster_size_pos: 0, + cluster_ts_ms: 0, + cues: Vec::new(), + frame_count: 0, + codec_private_slots, + codec_private_filled, + }) + } + + /// Write a single frame. + 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 + if !self.cluster_open || (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS { + if self.cluster_open { + // Close current cluster (it's a master with unknown size — we use known size) + // Actually, for streaming we keep clusters open-ended. Just start a new one. + } + self.start_cluster(pts_ms)?; + + // Add cue point at cluster start for keyframes (video track 0) + if keyframe && track_idx == 0 { + self.cues.push(CuePoint { + timestamp_ms: pts_ms, + track: track_idx + 1, + cluster_pos: self.cluster_pos - self.segment_start, + }); + } + } + + // Write SimpleBlock + let relative_ts = (pts_ms - self.cluster_ts_ms) as i16; + self.write_simple_block(track_idx + 1, relative_ts, keyframe, data)?; + self.frame_count += 1; + + Ok(()) + } + + /// Finish the MKV file: write Cues element. + pub fn finish(mut self) -> io::Result<()> { + // Close final cluster + self.end_cluster()?; + + // Write Cues + if !self.cues.is_empty() { + let cues_pos = ebml::start_master(&mut self.writer, ebml::CUES)?; + for cue in &self.cues { + let cp_pos = ebml::start_master(&mut self.writer, ebml::CUE_POINT)?; + 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::end_master(&mut self.writer, ctp_pos)?; + ebml::end_master(&mut self.writer, cp_pos)?; + } + ebml::end_master(&mut self.writer, cues_pos)?; + } + + self.writer.flush()?; + Ok(()) + } + + /// Fill in a deferred codecPrivate for a track. + /// Seeks back to the placeholder, writes the actual data, restores position. + pub fn fill_codec_private(&mut self, track_idx: usize, data: &[u8]) -> io::Result<()> { + if track_idx >= self.codec_private_filled.len() || self.codec_private_filled[track_idx] { + return Ok(()); + } + if let Some((pos, max_size)) = self.codec_private_slots[track_idx] { + if data.len() > max_size { + // Data too large for reserved space — can't fill in place + // This shouldn't happen with 256 bytes reserved + return Ok(()); + } + let current = self.writer.stream_position()?; + self.writer.seek(SeekFrom::Start(pos))?; + // Rewrite: element ID + size + data + zero-pad remainder + let mut padded = data.to_vec(); + padded.resize(max_size, 0); + ebml::write_binary(&mut self.writer, ebml::CODEC_PRIVATE, &padded)?; + self.writer.seek(SeekFrom::Start(current))?; + self.codec_private_filled[track_idx] = true; + } + Ok(()) + } + + fn start_cluster(&mut self, ts_ms: i64) -> io::Result<()> { + // Close previous cluster if open + if self.cluster_open { + self.end_cluster()?; + } + self.cluster_pos = self.writer.stream_position()?; + self.cluster_size_pos = ebml::start_master(&mut self.writer, ebml::CLUSTER)?; + ebml::write_uint(&mut self.writer, ebml::CLUSTER_TIMESTAMP, ts_ms as u64)?; + self.cluster_ts_ms = ts_ms; + self.cluster_open = true; + Ok(()) + } + + fn end_cluster(&mut self) -> io::Result<()> { + if self.cluster_open { + ebml::end_master(&mut self.writer, self.cluster_size_pos)?; + self.cluster_open = false; + } + Ok(()) + } + + 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 { + vec![(track_num as u8) | 0x80] + } else { + vec![0x40 | ((track_num >> 8) as u8), track_num as u8] + }; + + let flags: u8 = if keyframe { 0x80 } else { 0x00 }; + + let block_size = track_vint.len() + 2 + 1 + data.len(); // vint + ts(2) + flags(1) + data + ebml::write_id(&mut self.writer, ebml::SIMPLE_BLOCK)?; + ebml::write_size(&mut self.writer, block_size as u64)?; + self.writer.write_all(&track_vint)?; + self.writer.write_all(&relative_ts.to_be_bytes())?; + self.writer.write_all(&[flags])?; + self.writer.write_all(data)?; + + Ok(()) + } +} + +// ============================================================ +// Helpers +// ============================================================ + +fn parse_resolution(s: &str) -> (u32, u32) { + if s.contains("2160") { (3840, 2160) } + else if s.contains("1080") { (1920, 1080) } + else if s.contains("720") { (1280, 720) } + else if s.contains("576") { (720, 576) } + else if s.contains("480") { (720, 480) } + else { (1920, 1080) } +} + +fn parse_sample_rate(s: &str) -> f64 { + if s.contains("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 } +} diff --git a/src/mux/mod.rs b/src/mux/mod.rs new file mode 100644 index 0000000..b8b603e --- /dev/null +++ b/src/mux/mod.rs @@ -0,0 +1,25 @@ +//! MKV muxing pipeline. +//! +//! Provides BD transport stream → MKV remuxing via composable streams. +//! +//! The main type is `MkvStream` — wraps any `Write + Seek` output, +//! receives raw BD-TS bytes via `write()`, outputs MKV. +//! +//! ```text +//! disc.rip(title, MkvStream::new(file, &title)) +//! ``` +//! +//! Components (for advanced use): +//! - `ts`: BD transport stream demuxer (192-byte packets → PES frames) +//! - `ebml`: EBML write primitives for Matroska container +//! - `mkv`: MKV muxer (tracks, clusters, blocks, cues) +//! - `codec`: Elementary stream parsers (frame boundaries, codec headers) + +pub mod ebml; +pub mod ts; +pub mod mkv; +pub mod codec; +pub mod lookahead; +pub mod stream; + +pub use stream::MkvStream; diff --git a/src/mux/stream.rs b/src/mux/stream.rs new file mode 100644 index 0000000..8a29758 --- /dev/null +++ b/src/mux/stream.rs @@ -0,0 +1,218 @@ +//! MkvStream — a Write adapter that demuxes BD-TS and writes MKV. +//! +//! ```rust +//! let output = MkvStream::new(file) +//! .title(&disc.titles[0]) +//! .max_buffer(20 * 1024 * 1024); +//! +//! disc.rip(0, output)?; +//! ``` + +use std::io::{self, Write, Seek}; +use super::ts::{TsDemuxer, PesPacket}; +use super::mkv::{MkvMuxer, MkvTrack}; +use super::codec::{self, CodecParser}; +use super::lookahead::{LookaheadBuffer, LookaheadState, DEFAULT_LOOKAHEAD_SIZE}; +use crate::disc::{Stream, Title}; + +/// Phase of the MkvStream. +#[derive(Debug, Clone, Copy, PartialEq)] +enum Phase { + /// Collecting data in lookahead buffer, scanning for codec setup. + Scanning, + /// Header written, streaming directly to muxer. + Streaming, +} + +/// MKV output stream. Implements `Write`. +pub struct MkvStream { + demuxer: TsDemuxer, + muxer: Option>, + writer: Option, + parsers: Vec<(u16, Box)>, + pid_to_track: Vec<(u16, usize)>, + tracks: Vec, + title_name: String, + duration_secs: f64, + lookahead: LookaheadBuffer, + phase: Phase, + video_tracks_pending: usize, +} + +impl MkvStream { + /// Create a new MkvStream wrapping an output writer. + pub fn new(writer: W) -> Self { + Self { + demuxer: TsDemuxer::new(&[]), + muxer: None, + writer: Some(writer), + parsers: Vec::new(), + pid_to_track: Vec::new(), + tracks: Vec::new(), + title_name: String::new(), + duration_secs: 0.0, + lookahead: LookaheadBuffer::new(DEFAULT_LOOKAHEAD_SIZE), + phase: Phase::Scanning, + video_tracks_pending: 0, + } + } + + /// Set the title metadata (streams, duration, name). Returns self. + pub fn title(mut self, title: &Title) -> Self { + let mut pids = Vec::new(); + + for stream in &title.streams { + let (pid, track, parser) = match stream { + Stream::Video(v) => { + self.video_tracks_pending += 1; + (v.pid, MkvTrack::video(v), codec::parser_for_codec(v.codec)) + } + Stream::Audio(a) => (a.pid, MkvTrack::audio(a), codec::parser_for_codec(a.codec)), + Stream::Subtitle(s) => (s.pid, MkvTrack::subtitle(s), codec::parser_for_codec(s.codec)), + }; + let track_idx = self.tracks.len(); + pids.push(pid); + self.pid_to_track.push((pid, track_idx)); + self.parsers.push((pid, parser)); + self.tracks.push(track); + } + + self.demuxer = TsDemuxer::new(&pids); + self.title_name = title.playlist.clone(); + self.duration_secs = title.duration_secs; + self + } + + /// Set the lookahead buffer size in bytes. Default 5 MB. Returns self. + pub fn max_buffer(mut self, size: usize) -> Self { + self.lookahead = LookaheadBuffer::new(size); + self + } + + /// Finalize the MKV file — close cluster, write cues. + pub fn finish(mut self) -> io::Result<()> { + if let Some(ref mut muxer) = self.muxer { + let remaining = self.demuxer.flush(); + for pes in &remaining { + Self::process_one_pes(&self.pid_to_track, &mut self.parsers, muxer, pes)?; + } + } + if let Some(muxer) = self.muxer { + muxer.finish()?; + } + Ok(()) + } + + fn check_codec_private(&mut self) -> bool { + if self.video_tracks_pending == 0 { + return true; + } + for (pid, parser) in &self.parsers { + if let Some(cp) = parser.codec_private() { + if let Some((_, track_idx)) = self.pid_to_track.iter().find(|(p, _)| p == pid) { + if self.tracks[*track_idx].codec_private.is_none() { + self.tracks[*track_idx].codec_private = Some(cp); + self.video_tracks_pending -= 1; + } + } + } + } + self.video_tracks_pending == 0 + } + + fn start_streaming(&mut self) -> io::Result<()> { + let writer = self.writer.take().ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "writer already consumed") + })?; + + let muxer = MkvMuxer::new(writer, &self.tracks, Some(&self.title_name), self.duration_secs)?; + self.muxer = Some(muxer); + self.phase = Phase::Streaming; + + // Re-parse and write buffered data + let buffered = self.lookahead.drain(); + if !buffered.is_empty() { + let pids: Vec = self.pid_to_track.iter().map(|(pid, _)| *pid).collect(); + let mut temp_demuxer = TsDemuxer::new(&pids); + let mut packets = temp_demuxer.feed(&buffered); + packets.extend(temp_demuxer.flush()); + + if let Some(ref mut muxer) = self.muxer { + for pes in &packets { + Self::process_one_pes(&self.pid_to_track, &mut self.parsers, muxer, pes)?; + } + } + } + + Ok(()) + } + + fn process_one_pes( + pid_to_track: &[(u16, usize)], + parsers: &mut [(u16, Box)], + muxer: &mut MkvMuxer, + pes: &PesPacket, + ) -> io::Result<()> { + let track_idx = match pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) { + Some((_, idx)) => *idx, + None => return Ok(()), + }; + let parser = match parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) { + Some((_, p)) => p, + None => return Ok(()), + }; + let frames = parser.parse(pes); + for frame in frames { + muxer.write_frame(track_idx, frame.pts_ns, frame.keyframe, &frame.data)?; + } + Ok(()) + } +} + +impl Write for MkvStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self.phase { + Phase::Scanning => { + // Parse for codec info + let packets = self.demuxer.feed(buf); + for pes in &packets { + if let Some((_, parser)) = self.parsers.iter_mut().find(|(p, _)| *p == pes.pid) { + let _ = parser.parse(pes); + } + } + + // Try to buffer + let state = self.lookahead.push(buf); + + // Check if we have everything + if self.check_codec_private() { + self.lookahead.mark_ready(); + self.start_streaming()?; + return Ok(buf.len()); + } + + match state { + LookaheadState::Collecting => Ok(buf.len()), + LookaheadState::Overflow => Err(io::Error::new( + io::ErrorKind::OutOfMemory, + "MKV lookahead buffer overflow — no codec data found within buffer limit", + )), + LookaheadState::Ready => Ok(buf.len()), + } + } + Phase::Streaming => { + let packets = self.demuxer.feed(buf); + if let Some(ref mut muxer) = self.muxer { + for pes in &packets { + Self::process_one_pes(&self.pid_to_track, &mut self.parsers, muxer, pes)?; + } + } + Ok(buf.len()) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/mux/ts.rs b/src/mux/ts.rs new file mode 100644 index 0000000..526a83e --- /dev/null +++ b/src/mux/ts.rs @@ -0,0 +1,269 @@ +//! BD Transport Stream demuxer. +//! +//! Blu-ray uses 192-byte TS packets (not standard 188): +//! - 4-byte TP_extra_header (arrival timestamp + copy permission) +//! - 188-byte standard MPEG-TS packet +//! +//! This demuxer extracts PES packets from selected PIDs, with PTS/DTS timestamps. + +/// BD transport stream packet size (4-byte extra header + 188-byte TS). +const BD_TS_PACKET_SIZE: usize = 192; + +/// Standard TS packet size. +const TS_PACKET_SIZE: usize = 188; + +/// TS sync byte. +const SYNC_BYTE: u8 = 0x47; + +/// A reassembled PES packet with timestamp info. +#[derive(Debug)] +pub struct PesPacket { + /// MPEG-TS PID this packet belongs to. + pub pid: u16, + /// Presentation timestamp in 90kHz ticks (if present). + pub pts: Option, + /// Decode timestamp in 90kHz ticks (if present). + pub dts: Option, + /// Elementary stream data (video frame, audio frame, subtitle segment, etc.). + pub data: Vec, +} + +/// Per-PID PES reassembly state. +struct PesAssembler { + pid: u16, + buffer: Vec, + pts: Option, + dts: Option, + active: bool, +} + +impl PesAssembler { + fn new(pid: u16) -> Self { + Self { + pid, + buffer: Vec::with_capacity(256 * 1024), + pts: None, + dts: None, + active: false, + } + } + + /// Start a new PES packet. Returns the completed previous packet (if any). + fn start(&mut self, pts: Option, dts: Option) -> Option { + let completed = if self.active && !self.buffer.is_empty() { + Some(PesPacket { + pid: self.pid, + pts: self.pts, + dts: self.dts, + data: std::mem::replace(&mut self.buffer, Vec::with_capacity(256 * 1024)), + }) + } else { + self.buffer.clear(); + None + }; + self.pts = pts; + self.dts = dts; + self.active = true; + completed + } + + /// Append payload data to the current PES packet. + fn push(&mut self, data: &[u8]) { + if self.active { + self.buffer.extend_from_slice(data); + } + } + + /// Flush remaining data as a PES packet. + fn flush(&mut self) -> Option { + if self.active && !self.buffer.is_empty() { + self.active = false; + Some(PesPacket { + pid: self.pid, + pts: self.pts, + dts: self.dts, + data: std::mem::take(&mut self.buffer), + }) + } else { + None + } + } +} + +/// BD Transport Stream demuxer. +pub struct TsDemuxer { + assemblers: Vec, + pid_index: [i16; 8192], // PID → index into assemblers, -1 = not tracked +} + +impl TsDemuxer { + /// Create a new demuxer tracking the given PIDs. + pub fn new(pids: &[u16]) -> Self { + let mut pid_index = [-1i16; 8192]; + let mut assemblers = Vec::with_capacity(pids.len()); + for (i, &pid) in pids.iter().enumerate() { + pid_index[pid as usize] = i as i16; + assemblers.push(PesAssembler::new(pid)); + } + Self { assemblers, pid_index } + } + + /// Feed a chunk of BD transport stream data (must be aligned to 192-byte packets). + /// Returns completed PES packets. + pub fn feed(&mut self, data: &[u8]) -> Vec { + let mut completed = Vec::new(); + let mut offset = 0; + + while offset + BD_TS_PACKET_SIZE <= data.len() { + let packet = &data[offset..offset + BD_TS_PACKET_SIZE]; + offset += BD_TS_PACKET_SIZE; + + // Skip 4-byte TP_extra_header, check sync byte + if packet[4] != SYNC_BYTE { + continue; + } + + let ts = &packet[4..]; // 188-byte standard TS packet + + // Parse TS header + let pid = (((ts[1] & 0x1F) as u16) << 8) | ts[2] as u16; + let pusi = ts[1] & 0x40 != 0; // Payload Unit Start Indicator + let adaptation = (ts[3] >> 4) & 0x03; + + // Check if we're tracking this PID + let idx = self.pid_index[pid as usize]; + if idx < 0 { + continue; + } + let asm = &mut self.assemblers[idx as usize]; + + // Find payload start (skip adaptation field if present) + let payload_start = if adaptation == 0x03 || adaptation == 0x02 { + // Adaptation field present + let af_len = ts[4] as usize; + 5 + af_len + } else { + 4 + }; + + if payload_start >= TS_PACKET_SIZE { + continue; + } + + // No payload + if adaptation == 0x02 { + continue; + } + + let payload = &ts[payload_start..]; + + if pusi { + // New PES packet starts here — parse PES header + let (pts, dts, pes_data_start) = parse_pes_header(payload); + if let Some(prev) = asm.start(pts, dts) { + completed.push(prev); + } + if pes_data_start < payload.len() { + asm.push(&payload[pes_data_start..]); + } + } else { + // Continuation of current PES packet + asm.push(payload); + } + } + + completed + } + + /// Flush all assemblers, returning any remaining PES packets. + pub fn flush(&mut self) -> Vec { + let mut completed = Vec::new(); + for asm in &mut self.assemblers { + if let Some(pkt) = asm.flush() { + completed.push(pkt); + } + } + completed + } +} + +/// Parse a PES packet header, extracting PTS and DTS. +/// Returns (pts, dts, offset_to_elementary_stream_data). +fn parse_pes_header(data: &[u8]) -> (Option, Option, usize) { + // PES packet: 00 00 01 [stream_id] [length:2] [flags...] + if data.len() < 9 || data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x01 { + return (None, None, 0); + } + + let stream_id = data[3]; + + // 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 + { + return (None, None, 6); + } + + // Standard PES header: [6] = flags1, [7] = flags2, [8] = header_data_length + if data.len() < 9 { + return (None, None, 6); + } + + let pts_dts_flags = (data[7] >> 6) & 0x03; + let header_data_len = data[8] as usize; + let data_start = 9 + header_data_len; + + let mut pts = None; + let mut dts = None; + + if pts_dts_flags >= 2 && data.len() >= 14 { + pts = Some(parse_timestamp(&data[9..14])); + } + if pts_dts_flags == 3 && data.len() >= 19 { + dts = Some(parse_timestamp(&data[14..19])); + } + + (pts, dts, data_start) +} + +/// Parse a 5-byte PTS/DTS timestamp (33 bits in 90kHz). +fn parse_timestamp(data: &[u8]) -> i64 { + let b0 = data[0] as i64; + let b1 = data[1] as i64; + let b2 = data[2] as 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 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_timestamp() { + // Example: PTS = 0 → encoded as 21 00 01 00 01 + let data = [0x21, 0x00, 0x01, 0x00, 0x01]; + assert_eq!(parse_timestamp(&data), 0); + + // Example: PTS = 90000 (1 second at 90kHz) + // Manual encoding: 33 bits = 0x00015F90 + // This is just a sanity check that the parser doesn't crash + let data2 = [0x21, 0x00, 0x07, 0xE9, 0x01]; // approximate + let pts = parse_timestamp(&data2); + assert!(pts >= 0); + } + + #[test] + fn test_demuxer_empty() { + let mut demux = TsDemuxer::new(&[0x1011]); + let result = demux.feed(&[]); + assert!(result.is_empty()); + } +}