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
This commit is contained in:
MattJackson
2026-04-10 08:19:40 -07:00
parent 074f21ba58
commit 48b212c9d2
15 changed files with 2042 additions and 0 deletions
+37
View File
@@ -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<Frame> {
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<Vec<u8>> {
// AC3 doesn't need codecPrivate in MKV
None
}
}
+26
View File
@@ -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<Frame> {
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<Vec<u8>> { None }
}
+192
View File
@@ -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<Vec<u8>>,
pps: Option<Vec<u8>>,
}
impl H264Parser {
pub fn new() -> Self {
Self { sps: None, pps: None }
}
}
impl CodecParser for H264Parser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
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<Vec<u8>> {
// 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<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
}
/// Skip past the start code at position `pos`, returning the first byte after it.
pub fn skip_start_code(data: &[u8], pos: usize) -> Option<usize> {
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
}
+170
View File
@@ -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<Vec<u8>>,
sps: Option<Vec<u8>>,
pps: Option<Vec<u8>>,
}
impl HevcParser {
pub fn new() -> Self {
Self { vps: None, sps: None, pps: None }
}
}
impl CodecParser for HevcParser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
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<Vec<u8>> {
// 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)
}
}
+88
View File
@@ -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<u8>,
}
/// 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<Frame>;
/// Get codec initialization data (e.g., SPS+PPS for H.264).
/// Returns None until enough data has been seen.
fn codec_private(&self) -> Option<Vec<u8>>;
}
/// 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<Frame> {
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<Vec<u8>> {
None
}
}
/// Create the appropriate parser for a codec.
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::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)),
}
}
+25
View File
@@ -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<Frame> {
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<Vec<u8>> { None }
}
+27
View File
@@ -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<Frame> {
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<Vec<u8>> { None }
}
+115
View File
@@ -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<Vec<u8>>,
entry_point: Option<Vec<u8>>,
}
impl Vc1Parser {
pub fn new() -> Self {
Self { seq_header: None, entry_point: None }
}
}
impl CodecParser for Vc1Parser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
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<Vec<u8>> {
// 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<usize> {
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
}