From: Kostya Shishkov Date: Wed, 28 Oct 2026 16:33:55 +0000 (+0100) Subject: initial stab at Matroska/WebMKV demuxer X-Git-Url: https://git.nihav.org/?a=commitdiff_plain;h=4c4bf58b62b1ee7fb07e4fce681bc837cda081b4;p=nihav.git initial stab at Matroska/WebMKV demuxer --- diff --git a/nihav-commonfmt/Cargo.toml b/nihav-commonfmt/Cargo.toml index af8112f..935189f 100644 --- a/nihav-commonfmt/Cargo.toml +++ b/nihav-commonfmt/Cargo.toml @@ -33,12 +33,14 @@ muxers = [] all_demuxers = [ "demuxer_avi", "demuxer_gif", + "demuxer_mkv", "demuxer_mov", "demuxer_wav", "demuxer_y4m" ] demuxer_avi = ["demuxers"] demuxer_gif = ["demuxers"] +demuxer_mkv = ["demuxers"] demuxer_mov = ["demuxers"] demuxer_wav = ["demuxers"] demuxer_y4m = ["demuxers"] diff --git a/nihav-commonfmt/src/demuxers/mkv/mod.rs b/nihav-commonfmt/src/demuxers/mkv/mod.rs new file mode 100644 index 0000000..cb415c6 --- /dev/null +++ b/nihav-commonfmt/src/demuxers/mkv/mod.rs @@ -0,0 +1,379 @@ +use nihav_core::demuxers::*; + +#[allow(clippy::neg_cmp_op_on_partial_ord)] +mod parse; +use parse::*; + +const TIMEBASE: u32 = 1_000_000_000; + +struct MSeekInfo { + time: u64, + tracks: Vec<(u32, u64, u64)>, +} + +#[allow(dead_code)] +struct Track { + id: u32, + codec: String, + lacing: bool, +} + +struct Segment { + start: u64, + end: u64, + clu_start: u64, + duration: f64, + tscale: u32, + tracks: Vec, + seek_tab: Vec, +} + +impl std::convert::From for Segment { + fn from(mut seg: SegmentInfo) -> Self { + let tracks = seg.tracks.drain(..).map(|x| x.into()).collect(); + + Self { + start: seg.start, + end: seg.end, + clu_start: seg.clu_start, + duration: seg.duration, + tscale: seg.tscale, + tracks, + seek_tab: Vec::new(), + } + } +} + +impl std::convert::From for Track { + fn from(trk: TrackInfo) -> Self { + Self { + id: trk.number.unwrap() as u32, + lacing: trk.lacing.unwrap_or(false), + codec: trk.codec_id.unwrap(), + } + } +} + +impl std::convert::From for MSeekInfo { + fn from(cue: CueEntry) -> Self { + Self { + time: cue.time.unwrap(), + tracks: cue.tracks, + } + } +} + +struct MatroskaDemuxer<'a> { + src: &'a mut dyn ByteIO, + segments: Vec, + + cur_seg: usize, + seg_end: Option, + clu_end: Option, + clu_ts: Option, + + print_chunks: bool, +} + +impl<'a> DemuxCore<'a> for MatroskaDemuxer<'a> { + fn open(&mut self, strmgr: &mut StreamManager, seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let master_hdr = self.src.read_ebml_id()?; + validate!(master_hdr == 0x1A45DFA3); + let size = self.src.read_vint()?; + let hdr_end = self.src.tell() + size; + if self.print_chunks { + println!("master header size {size:X} @ 0"); + } + + let debuglevel = if self.print_chunks { Some(1) } else { None }; + let mut hdr = Header::default(); + parse_entries(&mut hdr, debuglevel, self.src, PARSE_MASTER_HEADER, hdr_end, ParseMode::Ignore)?; + validate!(hdr.version == Some(1) && hdr.read_ver.unwrap_or(1) == 1); + + self.segments = Vec::new(); + while let Ok(segment_id) = self.src.read_ebml_id() { + let seg_start = self.src.tell() - 4; + validate!(segment_id == 0x18538067); + let size = self.src.read_vint()?; + let seg_end = self.src.tell() + size; + if self.print_chunks { + println!("segment start @ {seg_start:X}"); + } + + let mut segment = SegmentInfo::new(self.print_chunks, self.src.tell(), seg_end); + parse_entries(&mut segment, debuglevel, self.src, PARSE_SEGMENT, seg_end, ParseMode::StopAt(CLUSTER_ID))?; + segment.clu_start = self.src.tell(); + + let mut cues = Vec::new(); + if let Some(entry) = segment.seek_entries.iter().find(|x| x.0 == CUES_ID) { + if self.print_chunks { + println!("cues at @{:X}", entry.1 + segment.start); + } + let _ = parse_cues(self.src, entry.1 + segment.start, seg_end, self.print_chunks, &mut cues); + } + + self.src.seek(SeekFrom::Start(seg_end))?; + segment.validate()?; + segment.fill_streams(strmgr)?; + let mut new_seg = Segment::from(segment); + new_seg.seek_tab = cues.drain(..).map(|x| x.into()).collect(); + let mut prev_time = None; + for cue in new_seg.seek_tab.iter() { + if let Some(pval) = prev_time { + validate!(pval < cue.time); + } + prev_time = Some(cue.time); + } + self.segments.push(new_seg); + } + validate!(!self.segments.is_empty()); + self.cur_seg = 0; + self.clu_end = None; + self.clu_ts = None; + seek_index.mode = SeekIndexMode::Automatic; + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + 'main_loop: loop { + if let Some(clu_end) = self.clu_end { + while self.src.tell() < clu_end { + let off = self.src.tell(); + let id = self.src.read_ebml_id()?; + let size = self.src.read_vint()?; + let end = self.src.tell() + size; + if self.print_chunks { + print_entry(2, off, id, size); + } + validate!(end <= clu_end); + match id { + CLUSTER_TIMESTAMP_ID => { + validate!(self.clu_ts.is_none()); + let ts = self.src.read_uint(size)?; + if self.print_chunks { + println!(" cluster timestamp {ts}"); + } + self.clu_ts = Some(ts); + }, + 0xA0 => return Err(DemuxerError::NotImplemented), // BlockGroup + 0xA3 => { + validate!(self.clu_ts.is_some()); + let trk_id = self.src.read_vint()?; + let timestamp = self.src.read_u16be()?; + let flags = self.src.read_byte()?; + let full_ts = self.clu_ts.unwrap() + u64::from(timestamp); + + let keyframe = (flags & 0x80) != 0; + let invisible = (flags & 0x08) != 0; + let lacing = (flags >> 1) & 3; + let disposable = (flags & 0x01) != 0; + if self.print_chunks { + println!(" simple block - track {trk_id} ts {full_ts} ({timestamp}) {}{}{}{}", if keyframe { " key" } else {""}, if invisible { " invisible" } else {""}, if lacing != 0 { " laced" } else {""}, if disposable { " disposable" } else {""}); + } + if trk_id > U32_MAX { return Err(DemuxerError::NotImplemented); } + let trk_id = trk_id as u32; + validate!(self.src.tell() < end); + if lacing != 0 { + if let Some(trk) = self.segments[self.cur_seg].tracks.iter().find(|x| x.id == trk_id) { + validate!(trk.lacing); + } + return Err(DemuxerError::NotImplemented); + } + if let Some(stream) = strmgr.get_stream_by_id(trk_id) { + let pkt_size = (end - self.src.tell()) as usize; + let ts = NATimeInfo::new(Some(full_ts), None, None, stream.tb_num, stream.tb_den); + return self.src.read_packet(stream, ts, keyframe, pkt_size); + } + }, + _ => {}, + } + validate!(self.src.tell() <= end); + self.src.seek(SeekFrom::Start(end))?; + } + self.clu_end = None; + } + if self.seg_end.is_none() { + if self.cur_seg >= self.segments.len() { + return Err(DemuxerError::EOF); + } + self.seg_end = Some(self.segments[self.cur_seg].end); + self.src.seek(SeekFrom::Start(self.segments[self.cur_seg].clu_start))?; + } + let seg_end = self.seg_end.unwrap(); + while self.src.tell() < seg_end { + let off = self.src.tell(); + let id = self.src.read_ebml_id()?; + let size = self.src.read_vint()?; + let end = self.src.tell() + size; + if self.print_chunks { + print_entry(1, off, id, size); + if id == CLUSTER_ID { + println!(" cluster"); + } + } + validate!(end <= seg_end); + if id == CLUSTER_ID { + self.clu_end = Some(end); + self.clu_ts = None; + continue 'main_loop; + } + self.src.seek(SeekFrom::Start(end))?; + } + self.seg_end = None; + self.cur_seg += 1; + } + } + + fn seek(&mut self, time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + let mut tgt_seg = None; + let mut entry_no = None; + for (seg_no, seg) in self.segments.iter().enumerate() { + let seek_time = match time { + NATimePoint::Milliseconds(ms) => NATimeInfo::rescale_ts(ms, 1, 1000, seg.tscale, TIMEBASE), + NATimePoint::PTS(ts) => ts, + _ => return Err(DemuxerError::SeekError), + }; + + for (i, entry) in seg.seek_tab.iter().enumerate() { + if seek_time >= entry.time { + tgt_seg = Some(seg_no); + entry_no = Some(i); + } else { + break; + } + } + } + if let (Some(seg_no), Some(entry_no)) = (tgt_seg, entry_no) { + self.seg_end = None; + self.clu_end = None; + self.clu_ts = None; + self.cur_seg = self.segments.len(); + + let tgt_seg = &self.segments[seg_no]; + + let (_, clu_pos, clu_off) = tgt_seg.seek_tab[entry_no].tracks[0]; + self.src.seek(SeekFrom::Start(tgt_seg.start + clu_pos))?; + let id = self.src.read_ebml_id()?; + let size = self.src.read_vint()?; + let clu_start = self.src.tell(); + let clu_end = clu_start + size; + validate!(id == CLUSTER_ID && clu_end <= tgt_seg.end && clu_off < size); + let mut ts = None; + while self.src.tell() < clu_end { + let id = self.src.read_ebml_id()?; + let size = self.src.read_vint()?; + let end = self.src.tell() + size; + validate!(end <= clu_end); + if id == CLUSTER_TIMESTAMP_ID { + ts = Some(self.src.read_uint(size)?); + break; + } + self.src.seek(SeekFrom::Start(end))?; + } + validate!(ts.is_some()); + self.src.seek(SeekFrom::Start(clu_start + clu_off))?; + self.cur_seg = seg_no; + self.seg_end = Some(tgt_seg.end); + self.clu_end = Some(clu_end); + self.clu_ts = ts; + + Ok(()) + } else { + Err(DemuxerError::SeekError) + } + } + fn get_duration(&self) -> u64 { + self.segments.iter().fold(0u64, + |acc, seg| acc + ((seg.duration * f64::from(seg.tscale) / f64::from(TIMEBASE / 1000)) as u64)) + } +} + +const PRINT_CHUNKS: &str = "print_chunks"; + +const DEMUXER_OPTIONS: &[NAOptionDefinition] = &[ + NAOptionDefinition { + name: PRINT_CHUNKS, + description: "Print parsed file structure", + opt_type: NAOptionDefinitionType::Bool }, +]; + +impl<'a> NAOptionHandler for MatroskaDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { DEMUXER_OPTIONS } + #[allow(clippy::single_match)] + fn set_options(&mut self, options: &[NAOption]) { + for option in options.iter() { + for opt_def in DEMUXER_OPTIONS.iter() { + if opt_def.check(option).is_ok() { + match (option.name, &option.value) { + (PRINT_CHUNKS, NAValue::Bool(val)) => { + self.print_chunks = *val; + }, + _ => {}, + } + } + } + } + } + fn query_option_value(&self, name: &str) -> Option { + match name { + PRINT_CHUNKS => Some(NAValue::Bool(self.print_chunks)), + _ => None, + } + } +} + +impl<'a> MatroskaDemuxer<'a> { + fn new(io: &'a mut dyn ByteIO) -> Self { + MatroskaDemuxer { + src: io, + print_chunks: false, + segments: Vec::new(), + cur_seg: 0, + seg_end: None, + clu_end: None, + clu_ts: None, + } + } +} + +pub struct MatroskaDemuxerCreator { } + +impl DemuxerCreator for MatroskaDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut dyn ByteIO) -> Box + 'a> { + Box::new(MatroskaDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "matroska" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + use nihav_codec_support::test::dmx_test::*; + + fn test_core(name: &str, nframes: usize, test_mode: DemuxerTestMode) { + let mut file = File::open(name).unwrap(); + let mut br = FileReader::new_read(&mut file); + let mut dmx = MatroskaDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + test_demuxer(&mut dmx, &mut sm, nframes, test_mode); + } + + #[test] + // sample from https://www.webmfiles.org/demo-files/ + fn test_webm() { + test_core("assets/Misc/big-buck-bunny_trailer.webm", 10, DemuxerTestMode::Test(vec![ + DPacket::new(1, Some( 0), None, 431, true), + DPacket::new(2, Some( 0), None, 22, false), + DPacket::new(2, Some( 13), None, 151, false), + DPacket::new(2, Some( 36), None, 144, false), + DPacket::new(1, Some( 40), None, 66, false), + DPacket::new(2, Some( 59), None, 155, false), + DPacket::new(1, Some( 80), None, 1339, false), + DPacket::new(2, Some( 82), None, 156, false), + DPacket::new(2, Some(105), None, 152, false), + DPacket::new(1, Some(120), None, 1415, false)])); + } +} diff --git a/nihav-commonfmt/src/demuxers/mkv/parse.rs b/nihav-commonfmt/src/demuxers/mkv/parse.rs new file mode 100644 index 0000000..cda0615 --- /dev/null +++ b/nihav-commonfmt/src/demuxers/mkv/parse.rs @@ -0,0 +1,760 @@ +use super::*; + +pub const CLUSTER_ID: u32 = 0x1F43B675; +pub const CLUSTER_TIMESTAMP_ID: u32 = 0xE7; +pub const CUES_ID: u32 = 0x1C53BB6B; + +pub const U32_MAX: u64 = u32::MAX as u64; + +fn gcd(mut a: u32, mut b: u32) -> u32 { + while a != 0 && b != 0 && a != b { + let minab = a.min(b); + a = a.max(b) - minab; + b = minab; + } + a.max(b) +} + +pub trait ReadEBML { + fn read_ebml_id(&mut self) -> DemuxerResult; + fn read_vint(&mut self) -> DemuxerResult; + fn read_uint(&mut self, size: u64) -> DemuxerResult; + fn read_float(&mut self, size: u64) -> DemuxerResult; +} + +impl ReadEBML for T { + fn read_ebml_id(&mut self) -> DemuxerResult { + let b = self.read_byte()?; + validate!(b != 0xFF); + let (len, id) = match b >> 3 { + 0 => return Err(DemuxerError::InvalidData), + 1 => { // 0000 1 + 37 bits + let low = self.read_u32be()?; + (37, (u64::from(b & 0x7) << 32) | u64::from(low)) + }, + 2 | 3 => { // 0001 + 28 bits + let low = self.read_u24be()?; + (28, (u64::from(b & 0xF) << 24) | u64::from(low)) + }, + 4..=7 => { // 001 + 21 bit + let low = self.read_u16be()?; + (21, (u64::from(b & 0x1F) << 16) | u64::from(low)) + }, + 8..=15 => { // 01 + 14 bits + let low = self.read_byte()?; + (14, (u64::from(b & 0x3F) << 8) | u64::from(low)) + }, + _ => { // 1 + 7 bits + (7, u64::from(b & 0x7F)) + } + }; + validate!(id != 0); + validate!((id >> 32) == 0); + Ok((1 << len) | (id as u32)) + } + fn read_vint(&mut self) -> DemuxerResult { + let b = self.read_byte()?; + match b { + 0 => Err(DemuxerError::InvalidData), + 0x80..=0xFF => { // 1 + 7 bits + Ok(u64::from(b & 0x7F)) + }, + 0x40..=0x7F => { // 01 + 14 bits + let low = self.read_byte()?; + Ok((u64::from(b & 0x3F) << 8) | u64::from(low)) + }, + 0x20..=0x3F => { // 001 + 21 bit + let low = self.read_u16be()?; + Ok((u64::from(b & 0x1F) << 16) | u64::from(low)) + }, + 0x10..=0x1F => { // 0001 + 28 bits + let low = self.read_u24be()?; + Ok((u64::from(b & 0xF) << 24) | u64::from(low)) + }, + 0x08..=0x0F => { // 0000 1 + 37 bits + let low = self.read_u32be()?; + Ok((u64::from(b & 0x7) << 32) | u64::from(low)) + }, + _ => { + let len = b.leading_zeros(); + let mut val = if len < 7 { u64::from((b << (len + 1)) >> (len + 1)) } else { 0 }; + for _ in 0..len { + val <<= 8; + val |= u64::from(self.read_byte()?); + } + Ok(val) + }, + } + } + fn read_uint(&mut self, size: u64) -> DemuxerResult { + match size { + 1 => Ok(u64::from(self.read_byte()?)), + 2 => Ok(u64::from(self.read_u16be()?)), + 3 => Ok(u64::from(self.read_u24be()?)), + 4 => Ok(u64::from(self.read_u32be()?)), + 5..=7 => Err(DemuxerError::NotImplemented), + 8 => Ok( self.read_u64be()?), + _ => Err(DemuxerError::InvalidData), + } + } + fn read_float(&mut self, size: u64) -> DemuxerResult { + match size { + 4 => Ok(f64::from(self.read_f32be()?)), + 8 => Ok( self.read_f64be()?), + _ => Err(DemuxerError::InvalidData), + } + } +} + +macro_rules! read_field { + (uint; $name:ident, $field:ident) => { + fn $name(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.$field.is_none()); + self.$field = Some(br.read_uint(size)?); + Ok(()) + } + }; + (float; $name:ident, $field:ident) => { + fn $name(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.$field.is_none()); + self.$field = Some(br.read_float(size)?); + Ok(()) + } + }; + (bool; $name:ident, $field:ident) => { + fn $name(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.$field.is_none()); + let val = br.read_uint(size)?; + validate!(val == 0 || val == 1); + self.$field = Some(val == 1); + Ok(()) + } + }; + (byte; $name:ident, $field:ident) => { + fn $name(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.$field.is_none()); + let val = br.read_uint(size)?; + validate!((0..=255).contains(&val)); + self.$field = Some(val as u8); + Ok(()) + } + }; +} + +#[derive(Clone,Copy,Debug,PartialEq)] +#[allow(dead_code)] +pub enum ParseMode { + Ignore, + Stop, + StopAt(u32), + StopAfter(u32), +} + +pub struct ParseEntry { + id: u32, + parse: fn (&mut T, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()>, +} + +pub fn print_entry(depth: u8, off: u64, id: u32, size: u64) { + for _ in 0..depth { + print!(" "); + } + println!("id {id:X} size {size:X} @ {off:X}"); +} + +pub fn parse_entries(obj: &mut T, debug: Option, br: &mut dyn ByteIO, entries: &[ParseEntry], end: u64, mode: ParseMode) -> DemuxerResult<()> { + while br.tell() < end { + let off = br.tell(); + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + if let Some(dlev) = debug { + print_entry(dlev, off, id, size); + } + if mode == ParseMode::StopAt(id) { + br.seek(SeekFrom::Start(off))?; + return Ok(()); + } + let cur_end = br.tell() + size; + validate!(cur_end <= end); + let mut found = false; + for pe in entries.iter() { + if id == pe.id { + (pe.parse)(obj, br, size)?; + found = true; + break; + } + } + validate!(br.tell() <= cur_end); + if !found { + match id { + 0xBF => { // CRC + validate!(size == 4); + br.read_u32be()?; + found = true; + }, + 0xEC => { // void + found = true; + }, + _ => {}, + } + } + br.seek(SeekFrom::Start(cur_end))?; + match mode { + ParseMode::Stop if !found => return Ok(()), + ParseMode::StopAfter(pid) if pid == id => return Ok(()), + _ => {}, + } + } + validate!(br.tell() == end); + Ok(()) +} + +#[derive(Default)] +pub struct Header { + pub version: Option, + pub read_ver: Option, +} + +pub static PARSE_MASTER_HEADER: &[ParseEntry
] = &[ + ParseEntry { id: 0x4286, parse: Header::parse_version }, + ParseEntry { id: 0x42F7, parse: Header::parse_read_version }, + ParseEntry { id: 0x42F2, parse: Header::parse_max_id }, + ParseEntry { id: 0x42F3, parse: Header::parse_max_size }, +]; + +impl Header { + fn parse_version(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 1 && self.version.is_none()); + self.version = Some(br.read_byte()?); + Ok(()) + } + fn parse_read_version(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 1 && self.read_ver.is_none()); + self.read_ver = Some(br.read_byte()?); + Ok(()) + } + fn parse_max_id(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 1); + let len = br.read_byte()?; + if len == 4 { Ok(()) } else { Err(DemuxerError::NotImplemented) } + } + fn parse_max_size(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 1); + let len = br.read_byte()?; + match len { + 0 => Err(DemuxerError::InvalidData), + 1..=8 => Ok(()), + _ => Err(DemuxerError::NotImplemented) + } + } +} + +#[derive(Default)] +pub struct SeekHead { + pub id: Option, + pub pos: Option, +} + +static PARSE_SEEK_HEAD: &[ParseEntry] = &[ + ParseEntry { id: 0x53AB, parse: SeekHead::parse_id }, + ParseEntry { id: 0x53AC, parse: SeekHead::parse_pos }, +]; + +impl SeekHead { + fn parse_id(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 4 && self.id.is_none()); + self.id = Some(br.read_u32be()?); + Ok(()) + } + read_field!(uint; parse_pos, pos); +} + +#[derive(Default)] +pub struct Info { + pub tscale: u32, + pub uuid: [u8; 16], + pub duration: f64, +} + +static PARSE_INFO: &[ParseEntry] = &[ + ParseEntry { id: 0x2AD7B1, parse: Info::parse_timescale }, + ParseEntry { id: 0x73A4, parse: Info::parse_uuid }, + ParseEntry { id: 0x4489, parse: Info::parse_duration }, +]; + +impl Info { + pub fn new() -> Self { + Self { tscale: 1000000, ..Default::default() } + } + fn parse_timescale(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let ts = br.read_uint(size)?; + validate!((1..=U32_MAX).contains(&ts)); + self.tscale = ts as u32; + Ok(()) + } + fn parse_uuid(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(size == 16); + br.read_buf(&mut self.uuid)?; + Ok(()) + } + fn parse_duration(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + self.duration = br.read_float(size)?; + Ok(()) + } +} + +#[derive(Default)] +pub struct TrackInfo { + pub debug: bool, + pub number: Option, + pub trktype: Option, + pub lacing: Option, + pub duration: Option, + pub tscale: Option, + pub codec_id: Option, + pub edata: Option>, + pub width: Option, + pub height: Option, + pub rate: Option, + pub real_rate: Option, + pub channels: Option, + pub adepth: Option, +} + +static PARSE_TRACK: &[ParseEntry] = &[ + ParseEntry { id: 0xD7, parse: TrackInfo::parse_number }, + //ParseEntry { id: 0x73C5, parse: TrackInfo::parse_uid }, + ParseEntry { id: 0x83, parse: TrackInfo::parse_type }, + ParseEntry { id: 0x9C, parse: TrackInfo::parse_lacing }, + ParseEntry { id: 0x23E383, parse: TrackInfo::parse_duration }, + ParseEntry { id: 0x23314F, parse: TrackInfo::parse_tscale }, + ParseEntry { id: 0x86, parse: TrackInfo::parse_codec_id }, + ParseEntry { id: 0x63A2, parse: TrackInfo::parse_edata }, + ParseEntry { id: 0xE0, parse: TrackInfo::parse_video_elem }, + ParseEntry { id: 0xE1, parse: TrackInfo::parse_audio_elem }, +]; + +static PARSE_TRACK_VIDEO_ELEM: &[ParseEntry] = &[ + ParseEntry { id: 0xB0, parse: TrackInfo::parse_width }, + ParseEntry { id: 0xBA, parse: TrackInfo::parse_height }, +]; + +static PARSE_TRACK_AUDIO_ELEM: &[ParseEntry] = &[ + ParseEntry { id: 0xB5, parse: TrackInfo::parse_sample_rate }, + ParseEntry { id: 0x78B5, parse: TrackInfo::parse_real_sample_rate }, + ParseEntry { id: 0x9F, parse: TrackInfo::parse_channels }, + ParseEntry { id: 0x6264, parse: TrackInfo::parse_adepth }, +]; + +impl TrackInfo { + pub fn validate(&self) -> DemuxerResult<()> { + validate!(self.number.is_some()); + if let Some(id) = self.number { + validate!(id > 0); + } + validate!(self.codec_id.is_some() && self.trktype.is_some()); + if let Some(ts) = self.tscale { + validate!(ts > 0.0); + } + if let Some(dur) = self.duration { + validate!(dur > 0); + } + match self.trktype { + Some(StreamType::Video) => { + validate!(self.width.is_some() && self.height.is_some()); + if let (Some(w), Some(h)) = (self.width, self.height) { + validate!((1..=16384).contains(&w) && (1..=16384).contains(&h)); + } + }, + Some(StreamType::Audio) => { + validate!(self.rate.is_some()); + if let Some(rate) = self.rate { + validate!((4000.0..=512000.0).contains(&rate)); + } + if let Some(rrate) = self.real_rate { + validate!((4000.0..=512000.0).contains(&rrate)); + } + if let Some(chan) = self.channels { + validate!((1..=128).contains(&chan)); + } + if let Some(depth) = self.adepth { + validate!((1..=64).contains(&depth)); + } + }, + _ => {}, + } + Ok(()) + } + + read_field!(uint; parse_number, number); + read_field!(uint; parse_duration, duration); + read_field!(float; parse_tscale, tscale); + read_field!(bool; parse_lacing, lacing); + read_field!(uint; parse_width, width); + read_field!(uint; parse_height, height); + read_field!(float; parse_sample_rate, rate); + read_field!(float; parse_real_sample_rate, real_rate); + read_field!(byte; parse_channels, channels); + read_field!(byte; parse_adepth, adepth); + + fn parse_type(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.trktype.is_none()); + let id = br.read_uint(size)?; + self.trktype = Some(match id { + 1 => StreamType::Video, + 2 => StreamType::Audio, + 17 => StreamType::Subtitles, + _ => StreamType::Data, + }); + Ok(()) + } + fn parse_codec_id(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.codec_id.is_none() && size > 0); + let mut c_id = String::with_capacity(size as usize); + for _ in 0..size { + let b = br.read_byte()?; + validate!((0x20..=0x7F).contains(&b)); + c_id.push(b as char); + } + self.codec_id = Some(c_id); + Ok(()) + } + fn parse_edata(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + validate!(self.edata.is_none() && (1..=(1 << 24)).contains(&size)); + let mut edata = vec![0; size as usize]; + br.read_buf(&mut edata)?; + self.edata = Some(edata); + Ok(()) + } + fn parse_video_elem(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let cur_end = br.tell() + size; + parse_entries(self, if self.debug { Some(3) } else { None }, br, PARSE_TRACK_VIDEO_ELEM, cur_end, ParseMode::Ignore)?; + Ok(()) + } + fn parse_audio_elem(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let cur_end = br.tell() + size; + parse_entries(self, if self.debug { Some(3) } else { None }, br, PARSE_TRACK_AUDIO_ELEM, cur_end, ParseMode::Ignore)?; + Ok(()) + } +} + + +#[derive(Default)] +pub struct CueEntry { + pub time: Option, + pub tracks: Vec<(u32, u64, u64)>, +} + +pub static PARSE_CUES: &[ParseEntry] = &[ + ParseEntry { id: 0xB3, parse: CueEntry::parse_time }, + ParseEntry { id: 0xB7, parse: CueEntry::parse_track_pos }, +]; + +impl CueEntry { + pub fn validate(&mut self) -> DemuxerResult<()> { + if self.time.is_some() && !self.tracks.is_empty() { + Ok(()) + } else { + Err(DemuxerError::InvalidData) + } + } + + read_field!(uint; parse_time, time); + #[allow(clippy::unnecessary_unwrap)] + fn parse_track_pos(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let end = br.tell() + size; + + let mut tid = None; + let mut pos = None; + let mut rpos = None; + while br.tell() + 2 <= end { + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + let cur_end = br.tell() + size; + validate!(cur_end <= end); + match id { + 0xF7 => { + if tid.is_some() { + validate!(pos.is_some() && rpos.is_some()); + self.tracks.push((tid.unwrap(), pos.unwrap(), rpos.unwrap())); + } + let ttid = br.read_uint(size)?; + validate!((1..=U32_MAX).contains(&ttid)); + tid = Some(ttid as u32); + }, + 0xF1 => { + validate!(tid.is_some() && pos.is_none()); + pos = Some(br.read_uint(size)?); + }, + 0xF0 => { + validate!(tid.is_some() && rpos.is_none()); + rpos = Some(br.read_uint(size)?); + }, + _ => {}, + } + validate!(br.tell() <= cur_end); + br.seek(SeekFrom::Start(cur_end))?; + } + if tid.is_some() && pos.is_some() && rpos.is_some() { + self.tracks.push((tid.unwrap(), pos.unwrap(), rpos.unwrap())); + } + Ok(()) + } +} + +#[derive(Default)] +pub struct SegmentInfo { + pub debug: bool, + pub start: u64, + pub end: u64, + pub clu_start: u64, + pub tracks: Vec, + pub seek_entries: Vec<(u32, u64)>, + pub tscale: u32, + pub duration: f64, +} + +pub static PARSE_SEGMENT: &[ParseEntry] = &[ + ParseEntry { id: 0x114D9B74, parse: SegmentInfo::parse_seek_table }, + ParseEntry { id: 0x1549A966, parse: SegmentInfo::parse_info }, + ParseEntry { id: 0x1654AE6B, parse: SegmentInfo::parse_tracks }, + ParseEntry { id: 0x1254C367, parse: SegmentInfo::skip }, // tags +]; + +impl SegmentInfo { + pub fn new(debug: bool, start: u64, end: u64) -> Self { + Self { debug, start, end, ..Default::default() } + } + pub fn validate(&self) -> DemuxerResult<()> { + validate!(!self.tracks.is_empty()); + Ok(()) + } + pub fn fill_streams(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<()> { + for trk in self.tracks.iter_mut() { + let id = trk.number.unwrap(); + if id > U32_MAX { return Err(DemuxerError::NotImplemented); } + let id = id as u32; + let ttype = trk.trktype.unwrap(); + let duration = trk.duration.unwrap_or_default(); + //let tscale = trk.tscale.unwrap_or(1.0); + let cid = trk.codec_id.as_ref().unwrap(); + let edata = trk.edata.take(); + let (cname, ctinfo) = match ttype { + StreamType::Video => { + let cname = match cid.as_str() { + "V_AV1" => "av1", + "V_AVS2" => "avs2", + "V_AVS3" => "avs3", + "V_CAVS" => "avs1", + "V_DIRAC" => "dirac", + "V_FFV1" => "ffv1", + "V_JPEG2000" => "jpeg2k", + "V_MJPEG" => "jpeg", + "V_MPEGH/ISO/HEVC" => "h265", + "V_MPEGI/ISO?VVC" => "h266", + "V_MPEG1" => "mpeg1", + "V_MPEG2" => "mpeg2", + "V_MPEG4/ISO/AVC" => "h264", + "V_MPEG4/ISO/AP" => "mpeg4asp", + "V_MPEG4/ISO/ASP" => "mpeg4asp", + "V_MPEG4/ISO/SP" => "mpeg4asp", + "V_MPEG4/MS/V3" => "msmpeg4v3", + "V_PRORES" => "prores", + "V_REAL/RV10" => "rv10", + "V_REAL/RV20" => "rv20", + "V_REAL/RV30" => "rv30", + "V_REAL/RV40" => "rv40", + "V_THEORA" => "theora", + "V_UNCOMPRESSED" => "rawvideo", + "V_VC1" => "vc1", + "V_VP8" => "vp8", + "V_VP9" => "vp9", + _ => "unknown", + }; + let width = trk.width.unwrap() as usize; + let height = trk.height.unwrap() as usize; + let vinfo = NAVideoInfo::new(width, height, false, RGB24_FORMAT); + (cname, NACodecTypeInfo::Video(vinfo)) + }, + StreamType::Audio => { + let cname = match cid.as_str() { + "A_AAC" | + "A_AAC/MPEG2/LC" | + "A_AAC/MPEG2/LC/SBR" | + "A_AAC/MPEG2/MAIN" | + "A_AAC/MPEG2/SSR" | + "A_AAC/MPEG4/LC" | + "A_AAC/MPEG4/LC/SBR" | + "A_AAC/MPEG4/LTP" | + "A_AAC/MPEG4/MAIN" | + "A_AAC/MPEG4/SSR" => "aac", + "A_AC3" | + "A_AC3/BSID9" | + "A_AC3/BSID10" => "ac3", + "A_ALAC" => "alac", + "A_ATRAC/AT1" => "atrac1", + "A_DTS" | + "A_DTS/EXPRESS" | + "A_DTS/LOSSLESS" => "dts", + "A_EAC3" => "eac3-multi", + "A_FLAC" => "flac", + "A_MLP" => "mlp", + "A_MPEG/L1" => "mp1", + "A_MPEG/L2" => "mp2", + "A_MPEG/L3" => "mp3", + "A_REAL/14_4" => "ra14.4", + "A_REAL/28_8" => "ra28.8", + "A_REAL/ATRC" => "atrac3", + "A_REAL/COOK" => "cook", + "A_REAL/RALF" => "ralf", + "A_REAL/SIPR" => "sipro", + "A_OPUS" => "opus", + "A_PCM/FLOAT/IEEE" | + "A_PCM/INT/BIG" | + "A_PCM/INT/LIT" => "pcm", + "A_QUICKTIME/QDMC" => "qdesign-music", + "A_QUICKTIME/QDM2" => "qdesign-music2", + "A_TRUEHD" => "mlp", + "A_TTA1" => "tta", + "A_VORBIS" => "vorbis", + "A_WAVPACK4" => "wavpack", + _ => "unknown", + }; + let rate = trk.rate.unwrap() as u32; + let channels = trk.channels.unwrap_or(1); + let adepth = trk.adepth.unwrap_or(32); + let mut fmt = match adepth { + 8 => SND_U8_FORMAT, + 16 => SND_S16_FORMAT, + _ => SND_F32P_FORMAT, + }; + match cid.as_str() { + "A_PCM/FLOAT/IEEE" => { + validate!(trk.adepth.is_some() && (adepth == 32 || adepth == 64)); + fmt.bits = adepth; + fmt.float = true; + fmt.be = false; + }, + "A_PCM/INT/BIG" => { + validate!(trk.adepth.is_some()); + fmt.bits = adepth; + fmt.be = true; + }, + "A_PCM/INT/LIT" => { + validate!(trk.adepth.is_some()); + fmt.bits = adepth; + fmt.be = false; + }, + _ => {} + } + let ainfo = NAAudioInfo::new(rate, channels, fmt, 1); + (cname, NACodecTypeInfo::Audio(ainfo)) + }, + _ => { + ("unknown", NACodecTypeInfo::None) + } + }; + let info = NACodecInfo::new(cname, ctinfo, edata); + + let div = gcd(self.tscale, TIMEBASE); + let (tb_num, tb_den) = (self.tscale / div, TIMEBASE / div); + let stream = NAStream::new(ttype, id, info, tb_num, tb_den, duration); + if strmgr.add_stream(stream).is_none() { + return Err(DemuxerError::InvalidData); + } + } + Ok(()) + } + + fn skip(&mut self, _br: &mut dyn ByteIO, _size: u64) -> DemuxerResult<()> { Ok(()) } + fn parse_seek_table(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let end = br.tell() + size; + let mut has_crc = false; + if self.debug { + println!(" seek table"); + } + while br.tell() + 2 <= end { + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + let cur_end = br.tell() + size; + validate!(cur_end <= end); + match id { + 0xBF => { + validate!(!has_crc); + validate!(size == 4); + has_crc = true; + }, + 0x4DBB => { + let mut seekhead = SeekHead::default(); + parse_entries(&mut seekhead, if self.debug { Some(2) } else { None }, br, PARSE_SEEK_HEAD, cur_end, ParseMode::Ignore)?; + validate!(seekhead.id.is_some() && seekhead.pos.is_some()); + if self.debug { + println!(" seek entry id {:X?} pos {:X?}", seekhead.id, seekhead.pos); + } + self.seek_entries.push((seekhead.id.unwrap(), seekhead.pos.unwrap())); + }, + _ => { + }, + } + validate!(br.tell() <= cur_end); + br.seek(SeekFrom::Start(cur_end))?; + } + Ok(()) + } + fn parse_info(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let cur_end = br.tell() + size; + if self.debug { + println!(" info block"); + } + let mut info = Info::new(); + parse_entries(&mut info, if self.debug { Some(2) } else { None }, br, PARSE_INFO, cur_end, ParseMode::Ignore)?; + self.duration = info.duration; + self.tscale = info.tscale; + + Ok(()) + } + fn parse_tracks(&mut self, br: &mut dyn ByteIO, size: u64) -> DemuxerResult<()> { + let end = br.tell() + size; + validate!(self.tracks.is_empty()); + while br.tell() < end { + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + let cur_end = br.tell() + size; + validate!(cur_end <= end); + if id == 0xAE { + if self.debug { + println!(" track"); + } + let mut trk = TrackInfo { debug: self.debug, ..Default::default() }; + parse_entries(&mut trk, if self.debug { Some(2) } else { None }, br, PARSE_TRACK, cur_end, ParseMode::Ignore)?; + trk.validate()?; + self.tracks.push(trk); + } + validate!(br.tell() <= cur_end); + br.seek(SeekFrom::Start(cur_end))?; + } + Ok(()) + } +} + +pub fn parse_cues(br: &mut dyn ByteIO, offset: u64, seg_end: u64, print_chunks: bool, cues: &mut Vec) -> DemuxerResult<()> { + br.seek(SeekFrom::Start(offset))?; + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + let cues_end = br.tell() + size; + validate!(id == CUES_ID && cues_end <= seg_end); + while br.tell() + 2 <= cues_end { + let id = br.read_ebml_id()?; + let size = br.read_vint()?; + let loc_end = br.tell() + size; + validate!(loc_end <= cues_end); + if id == 0xBB { + let mut cue = CueEntry::default(); + parse_entries(&mut cue, if print_chunks { Some(2) } else { None }, br, PARSE_CUES, loc_end, ParseMode::Ignore)?; + cue.validate()?; + cues.push(cue); + } + br.seek(SeekFrom::Start(loc_end))?; + } + Ok(()) +} diff --git a/nihav-commonfmt/src/demuxers/mod.rs b/nihav-commonfmt/src/demuxers/mod.rs index bcac6f3..e129bfc 100644 --- a/nihav-commonfmt/src/demuxers/mod.rs +++ b/nihav-commonfmt/src/demuxers/mod.rs @@ -15,6 +15,8 @@ macro_rules! validate { mod avi; #[cfg(feature="demuxer_gif")] mod gif; +#[cfg(feature="demuxer_mkv")] +mod mkv; #[cfg(feature="demuxer_mov")] mod mov; #[cfg(feature="demuxer_wav")] @@ -27,6 +29,8 @@ const DEMUXERS: &[&dyn DemuxerCreator] = &[ &avi::AVIDemuxerCreator {}, #[cfg(feature="demuxer_gif")] &gif::GIFDemuxerCreator {}, +#[cfg(feature="demuxer_mkv")] + &mkv::MatroskaDemuxerCreator {}, #[cfg(feature="demuxer_mov")] &mov::MOVDemuxerCreator {}, #[cfg(feature="demuxer_mov")] diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index 0c5d542..1fb4677 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -250,6 +250,11 @@ const DETECTORS: &[DetectConditions] = &[ conditions: &[CheckItem{offs: 0, cond: &CC::Or(&CC::Str(b"GIF87a"), &CC::Str(b"GIF89a")) }], }, + DetectConditions { + demux_name: "matroska", + extensions: ".mkv,.webm", + conditions: &[CheckItem{offs: 0, cond: &CC::Eq(Arg::U32BE(0x1A45DFA3)) }], + }, DetectConditions { demux_name: "mov", extensions: ".mov",