]> git.nihav.org Git - nihav.git/commitdiff
initial stab at Matroska/WebMKV demuxer
authorKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 28 Oct 2026 16:33:55 +0000 (17:33 +0100)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 28 Oct 2026 16:33:55 +0000 (17:33 +0100)
nihav-commonfmt/Cargo.toml
nihav-commonfmt/src/demuxers/mkv/mod.rs [new file with mode: 0644]
nihav-commonfmt/src/demuxers/mkv/parse.rs [new file with mode: 0644]
nihav-commonfmt/src/demuxers/mod.rs
nihav-registry/src/detect.rs

index af8112f44e16bbe6d3a030e0eea40a2d1aecfd9d..935189f4118d339f2991ba6f01e76c09140fb6c6 100644 (file)
@@ -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 (file)
index 0000000..cb415c6
--- /dev/null
@@ -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<Track>,
+    seek_tab:       Vec<MSeekInfo>,
+}
+
+impl std::convert::From<SegmentInfo> 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<TrackInfo> 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<CueEntry> 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<Segment>,
+
+    cur_seg:        usize,
+    seg_end:        Option<u64>,
+    clu_end:        Option<u64>,
+    clu_ts:         Option<u64>,
+
+    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<NAPacket> {
+        '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<NAValue> {
+        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<dyn DemuxCore<'a> + '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 (file)
index 0000000..cda0615
--- /dev/null
@@ -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<u32>;
+    fn read_vint(&mut self) -> DemuxerResult<u64>;
+    fn read_uint(&mut self, size: u64) -> DemuxerResult<u64>;
+    fn read_float(&mut self, size: u64) -> DemuxerResult<f64>;
+}
+
+impl<T: ?Sized + ByteIO> ReadEBML for T {
+    fn read_ebml_id(&mut self) -> DemuxerResult<u32> {
+        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<u64> {
+        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<u64> {
+        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<f64> {
+        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<T> {
+    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<T>(obj: &mut T, debug: Option<u8>, br: &mut dyn ByteIO, entries: &[ParseEntry<T>], 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<u8>,
+    pub read_ver:   Option<u8>,
+}
+
+pub static PARSE_MASTER_HEADER: &[ParseEntry<Header>] = &[
+    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<u32>,
+    pub pos:    Option<u64>,
+}
+
+static PARSE_SEEK_HEAD: &[ParseEntry<SeekHead>] = &[
+    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<Info>] = &[
+    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<u64>,
+    pub trktype:    Option<StreamType>,
+    pub lacing:     Option<bool>,
+    pub duration:   Option<u64>,
+    pub tscale:     Option<f64>,
+    pub codec_id:   Option<String>,
+    pub edata:      Option<Vec<u8>>,
+    pub width:      Option<u64>,
+    pub height:     Option<u64>,
+    pub rate:       Option<f64>,
+    pub real_rate:  Option<f64>,
+    pub channels:   Option<u8>,
+    pub adepth:     Option<u8>,
+}
+
+static PARSE_TRACK: &[ParseEntry<TrackInfo>] = &[
+    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<TrackInfo>] = &[
+    ParseEntry { id: 0xB0, parse: TrackInfo::parse_width },
+    ParseEntry { id: 0xBA, parse: TrackInfo::parse_height },
+];
+
+static PARSE_TRACK_AUDIO_ELEM: &[ParseEntry<TrackInfo>] = &[
+    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<u64>,
+    pub tracks:     Vec<(u32, u64, u64)>,
+}
+
+pub static PARSE_CUES: &[ParseEntry<CueEntry>] = &[
+    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<TrackInfo>,
+    pub seek_entries:   Vec<(u32, u64)>,
+    pub tscale:         u32,
+    pub duration:       f64,
+}
+
+pub static PARSE_SEGMENT: &[ParseEntry<SegmentInfo>] = &[
+    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<CueEntry>) -> 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(())
+}
index bcac6f30aa3808ff60bfc6131a933a9faef6fccb..e129bfc7fb9e65fca78b26222356f10c10a9ceb5 100644 (file)
@@ -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")]
index 0c5d5426e1be326e1376997dcb8e9e3a607dde11..1fb46771fb7b2c3feec6fca1c31569b0918575e5 100644 (file)
@@ -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",