From 174177460aca1f3ea4378c86ad996e0d94b4ae2f Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Fri, 19 Sep 2025 17:51:11 +0200 Subject: [PATCH] add DIB AVI demuxer --- nihav-allstuff/src/lib.rs | 1 + nihav-ms/Cargo.toml | 6 +- nihav-ms/src/demuxers/avidib.rs | 381 ++++++++++++++++++++++++++++++++ nihav-ms/src/demuxers/mod.rs | 27 +++ nihav-ms/src/lib.rs | 3 + nihav-registry/src/detect.rs | 8 + 6 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 nihav-ms/src/demuxers/avidib.rs create mode 100644 nihav-ms/src/demuxers/mod.rs diff --git a/nihav-allstuff/src/lib.rs b/nihav-allstuff/src/lib.rs index 034672c..544f63b 100644 --- a/nihav-allstuff/src/lib.rs +++ b/nihav-allstuff/src/lib.rs @@ -65,6 +65,7 @@ pub fn nihav_register_all_demuxers(rd: &mut RegisteredDemuxers) { indeo_register_all_demuxers(rd); llaudio_register_all_demuxers(rd); misc_register_all_demuxers(rd); + ms_register_all_demuxers(rd); rad_register_all_demuxers(rd); realmedia_register_all_demuxers(rd); vivo_register_all_demuxers(rd); diff --git a/nihav-ms/Cargo.toml b/nihav-ms/Cargo.toml index 4799eb8..c4cf728 100644 --- a/nihav-ms/Cargo.toml +++ b/nihav-ms/Cargo.toml @@ -15,7 +15,7 @@ path = "../nihav-codec-support" nihav_commonfmt = { path = "../nihav-commonfmt" } [features] -default = ["all_decoders", "all_encoders"] +default = ["all_decoders", "all_encoders", "all_demuxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] decoders = [] @@ -32,3 +32,7 @@ encoder_msvideo1 = ["encoders"] encoder_ms_adpcm = ["encoders"] encoder_ima_adpcm_ms = ["encoders"] encoders = [] + +all_demuxers = ["demuxer_avi_dib"] +demuxer_avi_dib = ["demuxers"] +demuxers = [] diff --git a/nihav-ms/src/demuxers/avidib.rs b/nihav-ms/src/demuxers/avidib.rs new file mode 100644 index 0000000..4852863 --- /dev/null +++ b/nihav-ms/src/demuxers/avidib.rs @@ -0,0 +1,381 @@ +use nihav_core::demuxers::*; +use nihav_core::demuxers::DemuxerError::*; +use std::str::FromStr; + +struct PalInfo { + pal: Arc<[u8; 1024]>, + changed: bool, +} + +#[derive(Default)] +struct AVIState { + key_offs: Vec, + size: usize, + movi_size: usize, + movi_pos: u64, + movi_orig: usize, + + got_vstream: bool, + pal: Option, + tb_num: u32, + nframes: u32, + cur_frame: u64, +} + +impl AVIState { + fn new() -> Self { + Self::default() + } + + fn parse_hdrl(&mut self, src: &mut dyn ByteIO, strmgr: &mut StreamManager, seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()> { + validate!(size > 20); + let end = src.tell() + (size as u64) - 4; + parse_chunks(self, src, strmgr, seek_index, HDRL_CHUNKS, end) + } + fn parse_movi(&mut self, src: &mut dyn ByteIO, _strmgr: &mut StreamManager, _seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()> { + validate!(size > 10); + self.movi_size = size - 4; + self.movi_orig = self.movi_size; + self.movi_pos = src.tell(); + Ok(()) + } + fn parse_idx1(&mut self, src: &mut dyn ByteIO, strmgr: &mut StreamManager, seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()> { + if seek_index.skip_index { return Ok(()); } +println!("index @ {:X}", src.tell()); + let _res = parse_idx1(src, strmgr, seek_index, size, self.movi_pos, &mut self.key_offs); + Ok(()) + } + + fn parse_hdra(&mut self, src: &mut dyn ByteIO, _strmgr: &mut StreamManager, _seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()> { + validate!(size >= 0x20); + self.tb_num = src.read_u32le()?; + validate!(self.tb_num > 0); + let _unk1 = src.read_u32le()?; + let _unk2 = src.read_u32le()?; + let _unk3 = src.read_u32le()?; + self.nframes = src.read_u32le()?; + + Ok(()) + } + fn parse_dibh(&mut self, src: &mut dyn ByteIO, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()> { + validate!(!self.got_vstream); + if size < 40 { return Err(InvalidData); } + validate!(self.tb_num > 0); + let bi_size = src.read_u32le()?; + if (bi_size as usize) < 40 { return Err(InvalidData); } + let width = src.read_u32le()?; + let height = src.read_u32le()? as i32; + let planes = src.read_u16le()?; + let bitcount = src.read_u16le()?; + let compression = src.read_tag()?; + let _img_size = src.read_u32le()?; + let _xdpi = src.read_u32le()?; + let _ydpi = src.read_u32le()?; + let colors = src.read_u32le()?; + validate!(colors <= 256); + let _imp_colors = src.read_u32le()?; + + let flip = height < 0; + let format = if bitcount > 8 { RGB24_FORMAT } else { PAL8_FORMAT }; + let mut vhdr = NAVideoInfo::new(width as usize, if flip { -height as usize } else { height as usize}, flip, format); + vhdr.bits = (planes as u8) * (bitcount as u8); + let cname = if find_raw_rgb_fmt(&compression, planes, bitcount, flip, &mut vhdr) { + "rawvideo-ms" + } else if compression == [1, 0, 0, 0] { + "msrle" + } else { + "unknown" + }; + let vci = NACodecTypeInfo::Video(vhdr); + if colors > 0 { + let mut pal = [0u8; 1024]; + let mut clr = [0; 4]; + for dpal in pal.chunks_mut(4).take(colors as usize) { + src.read_buf(&mut clr)?; + dpal[0] = clr[2]; + dpal[1] = clr[1]; + dpal[2] = clr[0]; + dpal[3] = 0; + } + self.pal = Some(PalInfo { pal: Arc::new(pal), changed: true }); + } + let vinfo = NACodecInfo::new(cname, vci, None); + let res = strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, self.tb_num, 1000000, u64::from(self.nframes))); + if res.is_none() { return Err(MemoryError); } + + self.got_vstream = true; + Ok(()) + } +} + +struct AVIDemuxer<'a> { + src: &'a mut dyn ByteIO, + state: AVIState, +} + +#[derive(Debug,Clone,Copy,PartialEq)] +enum RIFFTag { + Chunk([u8; 4]), + List([u8; 4]), +} + +struct ChunkHandler { + tag: RIFFTag, + parse: fn(obj: &mut T, src: &mut dyn ByteIO, strmgr: &mut StreamManager, seek_index: &mut SeekIndex, size: usize) -> DemuxerResult<()>, +} + +fn parse_chunks(obj: &mut T, src: &mut dyn ByteIO, strmgr: &mut StreamManager, seek_index: &mut SeekIndex, handlers: &[ChunkHandler], parse_end: u64) -> DemuxerResult<()> { + while src.tell() < parse_end { + let tag = src.read_tag()?; + let size = src.read_u32le()?; + let chunk_end = src.tell() + u64::from(size) + u64::from(size & 1); + validate!(chunk_end <= parse_end); + if &tag == b"JUNK" { + src.seek(SeekFrom::Start(chunk_end))?; + continue; + } + let ref_tag = if &tag == b"LIST" { + validate!(size >= 4); + RIFFTag::List(src.read_tag()?) + } else { RIFFTag::Chunk(tag) }; + + + if let Some(handler) = handlers.iter().find(|hdl| hdl.tag == ref_tag) { + (handler.parse)(obj, &mut *src, strmgr, seek_index, size as usize)?; + validate!(src.tell() <= chunk_end); + } + src.seek(SeekFrom::Start(chunk_end))?; + } + Ok(()) +} + +const AVI_ROOT_CHUNKS: &[ChunkHandler] = &[ + ChunkHandler{ tag: RIFFTag::List( *b"hdrl"), parse: AVIState::parse_hdrl }, + ChunkHandler{ tag: RIFFTag::List( *b"movi"), parse: AVIState::parse_movi }, + ChunkHandler{ tag: RIFFTag::Chunk(*b"idx1"), parse: AVIState::parse_idx1 }, +]; + +const HDRL_CHUNKS: &[ChunkHandler] = &[ + ChunkHandler{ tag: RIFFTag::Chunk(*b"hdra"), parse: AVIState::parse_hdra }, + ChunkHandler{ tag: RIFFTag::Chunk(*b"dibh"), parse: AVIState::parse_dibh }, +]; + +impl<'a> DemuxCore<'a> for AVIDemuxer<'a> { + fn open(&mut self, strmgr: &mut StreamManager, seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let riff_tag = self.src.read_tag()?; + let size = self.src.read_u32le()? as usize; + let avi_tag = self.src.read_tag()?; + validate!(&riff_tag == b"RIFF" && &avi_tag == b"AVI "); + self.state.size = size; + + match parse_chunks(&mut self.state, &mut *self.src, strmgr, seek_index, AVI_ROOT_CHUNKS, size as u64 + 8) { + Ok(()) => {}, + Err(DemuxerError::EOF) | Err(DemuxerError::IOError) => {}, + Err(err) => return Err(err), + } + + validate!(self.state.movi_pos != 0); + validate!(self.state.got_vstream); + + self.src.seek(SeekFrom::Start(self.state.movi_pos))?; + + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.state.movi_size == 0 { + return Err(EOF); + } + loop { + if (self.src.tell() & 1) == 1 { + self.src.read_skip(1)?; + self.state.movi_size -= 1; + if self.state.movi_size == 0 { + return Err(EOF); + } + } + let is_keyframe = self.state.key_offs.binary_search(&self.src.tell()).is_ok(); + let tag = self.src.read_tag()?; + let size = self.src.read_u32le()? as usize; + match &tag { + b"JUNK" => { + self.state.movi_size -= size + 8; + self.src.read_skip(size)?; + if self.state.movi_size == 0 { + return Err(EOF); + } + }, + b"LIST" => { + self.state.movi_size -= 12; + self.src.read_skip(4)?; + if self.state.movi_size == 0 { + return Err(EOF); + } + }, + b"idx1" | &[b'i', b'x', _, _] => { + return Err(EOF); + }, + b"dibc" => { + let stream = strmgr.get_stream(0); + if stream.is_none() { + self.src.read_skip(size)?; + self.state.movi_size -= size + 8; + continue; + } + let stream = stream.unwrap(); + if size == 0 { + self.state.movi_size -= 8; + if self.state.movi_size == 0 { + return Err(EOF); + } + continue; + } + let ts = stream.make_ts(Some(self.state.cur_frame), None, None); + let mut pkt = self.src.read_packet(stream, ts, is_keyframe, size)?; + if let Some(ref mut pe) = self.state.pal { + pkt.add_side_data(NASideData::Palette(pe.changed, pe.pal.clone())); + pe.changed = false; + } + self.state.cur_frame += 1; + self.state.movi_size -= size + 8; + return Ok(pkt); + }, + _ => { + self.src.read_skip(4)?; + if self.state.movi_size == 0 { + return Err(EOF); + } + } + } + } + } + + fn seek(&mut self, time: NATimePoint, seek_index: &SeekIndex) -> DemuxerResult<()> { + let ret = seek_index.find_pos(time); + if ret.is_none() { + return Err(DemuxerError::SeekError); + } + let seek_info = ret.unwrap(); + if seek_info.pos < self.state.movi_pos { return Err(DemuxerError::SeekError); } + let skip_size = (seek_info.pos - self.state.movi_pos) as usize; + if skip_size > self.state.movi_orig { return Err(DemuxerError::SeekError); } + self.state.movi_size = self.state.movi_orig - skip_size; + + self.state.cur_frame = seek_info.pts; + self.src.seek(SeekFrom::Start(seek_info.pos))?; + + Ok(()) + } + fn get_duration(&self) -> u64 { 0 } +} + +impl<'a> NAOptionHandler for AVIDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +impl<'a> AVIDemuxer<'a> { + fn new(io: &'a mut dyn ByteIO) -> Self { + AVIDemuxer { + src: io, + state: AVIState::new(), + } + } +} + +fn find_raw_rgb_fmt(compr: &[u8; 4], planes: u16, bitcount: u16, flip: bool, vhdr: &mut NAVideoInfo) -> bool { + match compr { + &[0, 0, 0, 0] | b"DIB " => { + if planes != 1 { + return false; + } + let fmt_name = match bitcount { + 8 => "pal8", + 16 => "bgr555", + 24 => "bgr24", + 32 => "bgra24", + _ => return false, + }; + if let Ok(fmt) = NAPixelFormaton::from_str(fmt_name) { + vhdr.format = fmt; + vhdr.flipped = !flip; + true + } else { + false + } + }, + _ => false, + } +} + +fn parse_idx1(src: &mut dyn ByteIO, strmgr: &mut StreamManager, seek_idx: &mut SeekIndex, size: usize, movi_pos: u64, key_offs: &mut Vec) -> DemuxerResult { + validate!((size & 15) == 0); + let num_entries = size >> 4; + let mut counter = 0u64; + let mut add_offset = 0; + let mut set_offset = false; + if let Some(stream) = strmgr.get_stream(0) { + for _ in 0..num_entries { + let tag = src.read_tag()?; + let _flags = src.read_u32le()?; + let mut offset = src.read_u32le()? as u64; + let _length = src.read_u32le()?; + + if !set_offset && offset < movi_pos { + add_offset = movi_pos - offset; + } + set_offset = true; + + offset += add_offset; + + if &tag == b"dibc" { + let (tb_num, tb_den) = stream.get_timebase(); + let pts = counter; + let time = NATimeInfo::ts_to_time(pts, 1000, tb_num, tb_den); + validate!(offset >= movi_pos); + seek_idx.add_entry(0, SeekEntry { time, pts, pos: offset }); + key_offs.push(offset); + counter += 1; + } + } + key_offs.sort_unstable(); + } + Ok(size) +} + +pub struct AVIDemuxerCreator { } + +impl DemuxerCreator for AVIDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut dyn ByteIO) -> Box + 'a> { + Box::new(AVIDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "avi-dib" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + #[test] + fn test_avi_dib_demux() { + //test sample from the beta AVI SDK + let mut file = File::open("assets/MS/CAR_CD.AVI").unwrap(); + let mut br = FileReader::new_read(&mut file); + let mut dmx = AVIDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if e == DemuxerError::EOF { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } +} diff --git a/nihav-ms/src/demuxers/mod.rs b/nihav-ms/src/demuxers/mod.rs new file mode 100644 index 0000000..7b55ed5 --- /dev/null +++ b/nihav-ms/src/demuxers/mod.rs @@ -0,0 +1,27 @@ +use nihav_core::demuxers::*; + + +#[allow(unused_macros)] +#[cfg(debug_assertions)] +macro_rules! validate { + ($a:expr) => { if !$a { println!("check failed at {}:{}", file!(), line!()); return Err(DemuxerError::InvalidData); } }; +} +#[cfg(not(debug_assertions))] +macro_rules! validate { + ($a:expr) => { if !$a { return Err(DemuxerError::InvalidData); } }; +} + +#[cfg(feature="demuxer_avi_dib")] +mod avidib; + +const DEMUXERS: &[&dyn DemuxerCreator] = &[ +#[cfg(feature="demuxer_avi_dib")] + &avidib::AVIDemuxerCreator {}, +]; + +/// Registers all available demuxers provided by this crate. +pub fn ms_register_all_demuxers(rd: &mut RegisteredDemuxers) { + for demuxer in DEMUXERS.iter() { + rd.add_demuxer(*demuxer); + } +} diff --git a/nihav-ms/src/lib.rs b/nihav-ms/src/lib.rs index 7cf1318..e62cc02 100644 --- a/nihav-ms/src/lib.rs +++ b/nihav-ms/src/lib.rs @@ -6,3 +6,6 @@ extern crate nihav_codec_support; mod codecs; pub use crate::codecs::ms_register_all_decoders; pub use crate::codecs::ms_register_all_encoders; + +mod demuxers; +pub use crate::demuxers::ms_register_all_demuxers; \ No newline at end of file diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index 54bc756..47468fb 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -198,6 +198,14 @@ struct DetectConditions<'a> { } const DETECTORS: &[DetectConditions] = &[ + DetectConditions { + demux_name: "avi-dib", + extensions: ".avi", + conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"RIFF") }, + CheckItem{offs: 8, cond: &CC::Str(b"AVI LIST")}, + CheckItem{offs: 20, cond: &CC::Str(b"hdrlhdra")}, + ] + }, DetectConditions { demux_name: "avi", extensions: ".avi", -- 2.39.5