]> git.nihav.org Git - nihav.git/commitdiff
introduce nihav_hlblocks, crate with common code for NihAV-based apps
authorKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 31 Jan 2026 14:43:38 +0000 (15:43 +0100)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 31 Jan 2026 14:43:38 +0000 (15:43 +0100)
nihav-hlblocks/Cargo.toml [new file with mode: 0644]
nihav-hlblocks/src/demux.rs [new file with mode: 0644]
nihav-hlblocks/src/imgseqdec.rs [new file with mode: 0644]
nihav-hlblocks/src/lib.rs [new file with mode: 0644]

diff --git a/nihav-hlblocks/Cargo.toml b/nihav-hlblocks/Cargo.toml
new file mode 100644 (file)
index 0000000..ee09e9a
--- /dev/null
@@ -0,0 +1,17 @@
+[package]
+name = "nihav_hlblocks"
+version = "0.1.0"
+authors = ["Kostya Shishkov <kostya.shishkov@gmail.com>"]
+edition = "2018"
+
+[dependencies.nihav_core]
+path = "../nihav-core"
+
+[dependencies.nihav_registry]
+path = "../nihav-registry"
+
+[features]
+default = []
+
+demuxer = []
+imgseq_dec = []
diff --git a/nihav-hlblocks/src/demux.rs b/nihav-hlblocks/src/demux.rs
new file mode 100644 (file)
index 0000000..707b14c
--- /dev/null
@@ -0,0 +1,556 @@
+//! Common interface and functionality for demuxing data.
+//!
+//! Currently there are three types of demuxing possible:
+//! * ordinary demuxer that returns packets from one or more embedded streams;
+//! * raw streams demuxer (e.g. MPEG-2 TS) that returns chunks of raw data and requires a packetiser to form them into proper packets;
+//! * raw stream without any additional headers (e.g. MP3) that also requires packetising.
+//!
+//! This module provides functionality to automatically detect input format and select appropriate
+//! implementation that will still behave like an ordinary `Demuxer` object that returns full packets.
+
+use std::io::SeekFrom;
+use nihav_core::codecs::*;
+use nihav_core::demuxers::*;
+use nihav_core::muxers::RegisteredMuxers;
+use nihav_registry::detect;
+use nihav_core::io::byteio::ByteIO;
+use nihav_core::sbbox::*;
+#[cfg(feature="imgseq_dec")]
+use crate::imgseqdec::ImgSeqDemuxer;
+
+/// A helper structure to pass references to all registered (de)muxers, encoders, decoders and such.
+///
+/// The most convenient way to populate it is to invoke `nihav_register_all_{demuxers,encoders,ets}` from `nihav_allstuff` crate on corresponding structure members.
+#[derive(Default)]
+pub struct FullRegister {
+    /// Registered demuxers
+    pub dmx_reg:    RegisteredDemuxers,
+    /// Registered raw stream demuxers
+    pub rdmx_reg:   RegisteredRawDemuxers,
+    /// Registered packetisers
+    pub pkt_reg:    RegisteredPacketisers,
+    /// Registered decoders
+    pub dec_reg:    RegisteredDecoders,
+    /// Registered muxers
+    pub mux_reg:    RegisteredMuxers,
+    /// Registered encoders
+    pub enc_reg:    RegisteredEncoders,
+}
+
+impl FullRegister {
+    /// Creates a new empty instance of `FullRegister`.
+    pub fn new() -> Self { Self::default() }
+}
+
+/// Auxiliary structure to deal with single input raw stream and packetising it. Not for the direct use.
+pub struct RawStreamCtx<'a> {
+    stream: NAStreamRef,
+    sm:     StreamManager,
+    pkt:    Box<dyn NAPacketiser + Send>,
+    br:     &'a mut dyn ByteIO,
+    pts:    u64,
+    seek:   SeekIndex,
+}
+
+impl<'a> RawStreamCtx<'a> {
+    fn new(stream: NAStreamRef, packetiser: Box<dyn NAPacketiser + Send>, br: &'a mut dyn ByteIO) -> Self {
+        let mut sm = StreamManager::new();
+        sm.add_stream_ref(stream.clone());
+        let mut seek = SeekIndex::new();
+        seek.add_stream(0);
+        Self { stream, pkt: packetiser, br, pts: 0, sm, seek }
+    }
+    fn account_for_packet(&mut self, packet: &mut NAPacket) {
+        let pos = self.br.tell() - (self.pkt.bytes_left() as u64);
+        if packet.get_pts().is_none() && packet.get_duration().is_some() {
+            packet.ts.pts = Some(self.pts);
+        }
+        if packet.is_keyframe() {
+            let pts = packet.get_pts().unwrap_or(self.pts);
+            let time = NATimeInfo::rescale_ts(pts, self.stream.tb_num, self.stream.tb_den, 1, 1000);
+            let in_range = if let Some(last) = self.seek.seek_info[0].entries.last() {
+                    last.pts >= pts
+                } else {
+                    false
+                };
+            if !in_range {
+                self.seek.add_entry(0, SeekEntry { time, pts, pos });
+            }
+        }
+        self.pts += packet.get_duration().unwrap_or(0);
+    }
+    fn get_frame(&mut self) -> DemuxerResult<NAPacket> {
+        let mut buf = [0; 1048576];
+        loop {
+            match self.pkt.get_packet(self.stream.clone()) {
+                Ok(Some(mut packet)) => {
+                    self.account_for_packet(&mut packet);
+                    return Ok(packet);
+                },
+                Ok(None) => {},
+                Err(DecoderError::ShortData) => {},
+                _ => return Err(DemuxerError::InvalidData),
+            };
+            match self.br.read_buf_some(&mut buf) {
+                Ok(size) => {
+                    self.pkt.add_data(&buf[..size]);
+                },
+                Err(_) => {
+                    match self.pkt.get_packet(self.stream.clone()) {
+                        Ok(Some(mut packet)) => {
+                            self.account_for_packet(&mut packet);
+                            return Ok(packet);
+                        },
+                        Ok(None) | Err(DecoderError::ShortData) => return Err(DemuxerError::EOF),
+                        _ => return Err(DemuxerError::InvalidData),
+                    };
+                },
+            };
+        }
+    }
+}
+
+/// Auxiliary structure to maintain the state of raw streams demuxer. Not for the direct use.
+pub struct RawDemuxerState<'a> {
+    dmx:    SBBox<ReaderBox, RawDemuxer<'a>>,
+    pkts:   Vec<Option<Box<dyn NAPacketiser + Send>>>,
+    eof:    bool,
+}
+
+/// Alias for input source
+pub type ReaderBox = Box<dyn ByteIO>;
+
+/// Wrapper that provides common interface a la `Demuxer` for different types of input.
+pub enum DemuxerObject<'a> {
+    /// Nothing created yet
+    None,
+    /// Ordinary demuxer
+    Normal(SBBox<ReaderBox, Demuxer<'a>>),
+    /// Raw stream demuxer with individual stream contexts
+    Raw(RawDemuxerState<'a>),
+    /// Elementary stream
+    RawStream(SBBox<ReaderBox, RawStreamCtx<'a>>),
+    #[cfg(feature="imgseq_dec")]
+    /// Image sequence
+    ImageSequence(ImgSeqDemuxer),
+}
+
+impl<'a> DemuxerObject<'a> {
+    fn new_demuxer(mut br: ReaderBox, reg: &FullRegister, name: &str, dmx_name: &str, opts: &[NAOption], verbose: bool, forced: bool) -> Result<DemuxerObject<'a>, ReaderBox> {
+        if let Some(dmx_fact) = reg.dmx_reg.find_demuxer(dmx_name) {
+            if verbose {
+                println!("{} demuxer {} on {}", if forced { "forcing" } else { "trying" }, dmx_name, name);
+            }
+            br.seek(SeekFrom::Start(0)).unwrap();
+            SelfBorrow::try_new(br, |br_| {
+                unsafe {
+                    create_demuxer_with_options(dmx_fact, (*br_).as_mut(), opts).ok()
+                }
+            }).map(DemuxerObject::Normal)
+        } else {
+            Err(br)
+        }
+    }
+    fn new_raw_demuxer(mut br: ReaderBox, reg: &FullRegister, name: &str, dmx_name: &str, opts: &[NAOption], verbose: bool, forced: bool) -> Result<DemuxerObject<'a>, ReaderBox> {
+        if let Some(rdmx_fact) = reg.rdmx_reg.find_demuxer(dmx_name) {
+            if verbose {
+                println!("{} raw demuxer {} on {}", if forced { "forcing" } else { "trying" }, dmx_name, name);
+            }
+            br.seek(SeekFrom::Start(0)).unwrap();
+            let dmx = SelfBorrow::try_new(br, |br_| {
+                    unsafe {
+                        create_raw_demuxer_with_options(rdmx_fact, (*br_).as_mut(), opts).ok()
+                    }
+                })?;
+            let mut pkts = Vec::new();
+            for stream in dmx.get_object().get_streams() {
+                if let Some(pcreate) = reg.pkt_reg.find_packetiser(stream.get_info().get_name()) {
+                    let mut packetiser = (pcreate)();
+                    packetiser.attach_stream(stream);
+                    pkts.push(Some(packetiser));
+                } else {
+                    pkts.push(None);
+                }
+            }
+            Ok(DemuxerObject::Raw(RawDemuxerState{ dmx, pkts, eof: false }))
+        } else {
+            Err(br)
+        }
+    }
+
+    /// Attempts to create a new instance of `DemuxerObject`.
+    ///
+    /// Input parameters:
+    /// * `br` --- input I/O context
+    /// * `reg` --- register with at least some of the demuxers or raw stream demuxers with packetisers being registered
+    /// * `name` --- input file name (used for format detection)
+    /// * `force_dmx` --- optionally provided demuxer name (set to `None` to have autodetection instead)
+    /// * `is_raw` --- skips attempting to create a demuxer and treats input as an elementary stream
+    /// * `opts` --- demuxer options
+    /// * `verbose` -- enables printing messages e.g. for debugging purposes
+    pub fn create(mut br: ReaderBox, reg: &FullRegister, name: &str, force_dmx: Option<&str>, is_raw: bool, opts: &[NAOption], verbose: bool) -> DemuxerObject<'a> {
+        if !is_raw {
+            if let Some(dmx_name) = force_dmx {
+                match Self::new_demuxer(br, reg, name, dmx_name, opts, verbose, true) {
+                    Ok(dmx) => return dmx,
+                    Err(nbr) => br = nbr,
+                };
+                if let Ok(dmx) = Self::new_raw_demuxer(br, reg, name, dmx_name, opts, verbose, true) {
+                    return dmx;
+                } else {
+                    return DemuxerObject::None;
+                }
+            }
+            let res = detect::detect_format(name, &mut *br);
+            let (dmx_name, _) = res.unwrap_or(("", detect::DetectionScore::No));
+            if !dmx_name.is_empty() {
+                match Self::new_demuxer(br, reg, name, dmx_name, opts, verbose, false) {
+                    Ok(dmx) => return dmx,
+                    Err(nbr) => br = nbr,
+                }
+            }
+            if !dmx_name.is_empty() {
+                match Self::new_raw_demuxer(br, reg, name, dmx_name, opts, verbose, false) {
+                    Ok(dmx) => return dmx,
+                    Err(nbr) => br = nbr,
+                }
+            }
+            for rdmx in reg.rdmx_reg.iter() {
+                if rdmx.check_format(&mut *br) {
+                    if verbose {
+                        println!("detected {} as {}", name, rdmx.get_name());
+                    }
+                    match Self::new_raw_demuxer(br, reg, name, rdmx.get_name(), opts, false, false) {
+                        Ok(dmx) => return dmx,
+                        Err(nbr) => br = nbr,
+                    }
+                }
+            }
+        }
+        br.seek(SeekFrom::Start(0)).unwrap();
+        let mut buf = vec![0; 1048576];
+        let size = br.read_buf_some(&mut buf).unwrap();
+        br.seek(SeekFrom::Start(0)).unwrap();
+        let mut pname = "";
+
+        for pinfo in reg.pkt_reg.iter() {
+            let mut packetiser = (pinfo.get_packetiser)();
+            packetiser.add_data(&buf[..size]);
+            if packetiser.parse_stream(0).is_ok() {
+                pname = pinfo.name;
+                break;
+            }
+        }
+        if !pname.is_empty() {
+            if verbose {
+                println!("found raw stream of type {} for {}", pname, name);
+            }
+            let pcreate = reg.pkt_reg.find_packetiser(pname).unwrap();
+            let rctx = SelfBorrow::new(br, |br_| {
+                    unsafe {
+                        let mut packetiser = (pcreate)();
+                        packetiser.add_data(&buf[..size]);
+                        let stream = packetiser.parse_stream(0).unwrap();
+                        packetiser.reset();
+                        RawStreamCtx::new(stream, packetiser, (*br_).as_mut())
+                    }
+                });
+            DemuxerObject::RawStream(rctx)
+        } else {
+            DemuxerObject::None
+        }
+    }
+    /// Wraps image sequence demuxer.
+    #[cfg(feature="imgseq_dec")]
+    pub fn create_imgseq(isd: ImgSeqDemuxer) -> Self {
+        DemuxerObject::ImageSequence(isd)
+    }
+    /// Checks if there is no actual demuxer created.
+    pub fn is_none(&self) -> bool {
+        matches!(*self, DemuxerObject::None)
+    }
+    /// Returns total file duration (or 0 if unknown).
+    pub fn get_duration(&self) -> u64 {
+        match *self {
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().get_duration(),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().get_duration(),
+            DemuxerObject::RawStream(ref ctx) => {
+                let stream = &ctx.get_object().stream;
+                NATimeInfo::rescale_ts(stream.duration, stream.tb_num, stream.tb_den, 1, 1000)
+            },
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(_) => 0,
+            _ => 0,
+        }
+    }
+    /// Returns number of present streams.
+    pub fn get_num_streams(&self) -> usize {
+        match *self {
+            DemuxerObject::None => 0,
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().get_num_streams(),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().get_num_streams(),
+            DemuxerObject::RawStream(_) => 1,
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(_) => 1,
+        }
+    }
+    /// Returns stream with the requested index.
+    pub fn get_stream(&self, idx: usize) -> Option<NAStreamRef> {
+        match *self {
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().get_stream(idx),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().get_stream(idx),
+            DemuxerObject::RawStream(ref ctx) if idx == 0 => Some(ctx.get_object().stream.clone()),
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref ctx) if idx == 0 => Some(ctx.stream.clone()),
+            _ => None,
+        }
+    }
+    /// Returns stream manager associated with the demuxer.
+    pub fn get_stream_manager(&self) -> &StreamManager {
+        match *self {
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().get_stream_manager(),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().get_stream_manager(),
+            DemuxerObject::RawStream(ref ctx) => &ctx.get_object().sm,
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref ctx) => &ctx.sm,
+            _ => unreachable!(),
+        }
+    }
+    /// Demuxes a packet.
+    pub fn get_frame(&mut self) -> DemuxerResult<NAPacket> {
+        match *self {
+            DemuxerObject::Normal(ref mut dmx) => dmx.get_object_mut().get_frame(),
+            DemuxerObject::Raw(ref mut dmxs) => {
+                loop {
+                    let mut has_some = false;
+                    for (stream, p) in dmxs.dmx.get_object().get_streams().zip(dmxs.pkts.iter_mut()) {
+                        if let Some(ref mut pkts) = p {
+                            match pkts.get_packet(stream.clone()) {
+                                Ok(Some(pkt)) => return Ok(pkt),
+                                Ok(None) | Err(DecoderError::ShortData) => {
+                                    if dmxs.eof {
+                                        *p = None;
+                                    }
+                                },
+                                Err(err) => {
+                                    println!("packetisation error {:?}", err);
+                                    return Err(DemuxerError::InvalidData);
+                                }
+                            };
+                            has_some |= p.is_some();
+                        }
+                    }
+                    if !has_some {
+                        return Err(DemuxerError::EOF);
+                    }
+                    if let Ok(data) = dmxs.dmx.get_object_mut().get_data() {
+                        let id = data.get_stream().get_id();
+                        for (i, stream) in dmxs.dmx.get_object().get_streams().enumerate() {
+                            if stream.get_id() == id {
+                                if let Some(ref mut pkts) = dmxs.pkts[i] {
+                                    pkts.add_data(&data.get_buffer());
+                                }
+                                break;
+                            }
+                        }
+                    } else {
+                        dmxs.eof = true;
+                    }
+                }
+            },
+            DemuxerObject::RawStream(ref mut ctx) => ctx.get_object_mut().get_frame(),
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref mut ctx) => ctx.get_frame(),
+            _ => unreachable!(),
+        }
+    }
+    /// Seeks to the specified time.
+    pub fn seek(&mut self, seek_time: NATimePoint) -> DemuxerResult<()> {
+        match *self {
+            DemuxerObject::Normal(ref mut dmx) => dmx.get_object_mut().seek(seek_time),
+            DemuxerObject::Raw(ref mut dmxs) => dmxs.dmx.get_object_mut().seek(seek_time),
+            DemuxerObject::RawStream(ref mut ctxobj) => {
+                let ctx = ctxobj.get_object_mut();
+                if seek_time == NATimePoint::None {
+                    return Err(DemuxerError::SeekError);
+                }
+                if let Some(last) = ctx.seek.seek_info[0].entries.last() {
+                    let in_index = match seek_time {
+                            NATimePoint::None => unreachable!(),
+                            NATimePoint::PTS(pts) => last.pts >= pts,
+                            NATimePoint::Milliseconds(ms) => last.time >= ms,
+                        };
+                    if in_index {
+                        if let Some(result) = ctx.seek.find_pos(seek_time) {
+                            ctx.br.seek(SeekFrom::Start(result.pos))?;
+                            ctx.pts = result.pts;
+                            ctx.pkt.reset();
+                            return Ok(());
+                        }
+                    }
+                }
+                if let Some(last) = ctx.seek.seek_info[0].entries.last() {
+                    ctx.br.seek(SeekFrom::Start(last.pos))?;
+                    ctx.pts = last.pts;
+                    ctx.pkt.reset();
+                }
+                let mut key_pts = 0;
+                while let Ok(pkt) = ctx.get_frame() {
+                    if !pkt.ts.less_than(seek_time) && !pkt.ts.equal(seek_time) {
+                        break;
+                    }
+                    if pkt.is_keyframe() {
+                        key_pts = pkt.get_pts().unwrap_or(0);
+                    }
+                }
+                let result = ctx.seek.find_pos(NATimePoint::PTS(key_pts)).unwrap();
+                ctx.br.seek(SeekFrom::Start(result.pos))?;
+                ctx.pts = result.pts;
+                ctx.pkt.reset();
+                Ok(())
+            },
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref mut ctx) => ctx.seek(seek_time),
+            _ => Err(DemuxerError::NotImplemented),
+        }
+    }
+}
+
+
+impl<'a> NAOptionHandler for DemuxerObject<'a> {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] {
+        match *self {
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().get_supported_options(),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().get_supported_options(),
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref ctx) => ctx.get_supported_options(),
+            _ => &[],
+        }
+    }
+    fn set_options(&mut self, options: &[NAOption]) {
+        match *self {
+            DemuxerObject::Normal(ref mut dmx) => dmx.get_object_mut().set_options(options),
+            DemuxerObject::Raw(ref mut dmxs) => dmxs.dmx.get_object_mut().set_options(options),
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref mut ctx) => ctx.set_options(options),
+            _ => {},
+        }
+    }
+    fn query_option_value(&self, name: &str) -> Option<NAValue> {
+        match *self {
+            DemuxerObject::Normal(ref dmx) => dmx.get_object().query_option_value(name),
+            DemuxerObject::Raw(ref dmxs) => dmxs.dmx.get_object().query_option_value(name),
+            #[cfg(feature="imgseq_dec")]
+            DemuxerObject::ImageSequence(ref ctx) => ctx.query_option_value(name),
+            _ => None,
+        }
+    }
+}
+
+/// Checks input for various metadata tags.
+///
+/// Music files often have various metadata (e.g. title, artist, genre etc) associated with them but not always being a part of the file/stream format but rather a generic extension prepended or appended to the elementary stream.
+/// MP3 with ID3 tags is the most common example of such files.
+///
+/// This functions allows detecting several of such extensions reporting whether it expects stream to be raw, start position and end position (if necessary).
+/// For example, MP3 with first 4200 bytes being an ID3 tag will result in `(true, 4200, None)` value while FLAC file with APE tags at the end will return something like `(false, 0, Some(424242))`.
+pub fn detect_tags(br: &mut dyn ByteIO, verbose: bool) -> (bool, u64, Option<u64>) {
+    let mut is_raw = false;
+    let mut start = 0;
+    let mut end = None;
+
+    // check for ID3v{2-4}
+    let mut buf = [0; 5];
+    loop {
+        if br.peek_buf(&mut buf).is_err() {
+            break;
+        }
+        if &buf[0..3] == b"ID3" && buf[3] > 0 && buf[3] < 5 && buf[4] == 0 { //ID3 tag found, must be a raw stream
+            br.read_skip(6).unwrap();
+            let mut size = 0;
+            for _ in 0..4 {
+                let b = br.read_byte().unwrap();
+                if (b & 0x80) != 0 {
+                    if verbose {
+                        println!("Invalid ID3 size");
+                    }
+                    break;
+                }
+                size = (size << 7) | u64::from(b);
+            }
+            start += size + 10;
+            br.read_skip(size as usize).unwrap();
+            while let Ok(0) = br.read_byte() {
+                start += 1;
+            }
+            br.seek(SeekFrom::Start(start)).unwrap();
+            is_raw = true;
+        } else {
+            break;
+        }
+    }
+    // check for ID3v1
+    br.seek(SeekFrom::End(-128)).unwrap();
+    let off = br.tell();
+    br.peek_buf(&mut buf[..3]).unwrap();
+    if &buf[0..3] == b"TAG" {
+        end = Some(off);
+        // check for Lyrics v2
+        let mut sig = [0; 9];
+        br.seek(SeekFrom::End(-128 - 9)).unwrap();
+        br.peek_buf(&mut sig).unwrap();
+        if &sig == b"LYRICS200" {
+            br.seek(SeekFrom::Current(-6)).unwrap();
+            let mut sizestr = [0; 6];
+            br.peek_buf(&mut sizestr).unwrap();
+            if let Ok(sstr) = std::str::from_utf8(&sizestr) {
+                if let Ok(size) = sstr.parse::<u64>() {
+                    end = Some(br.tell() - size);
+                }
+            }
+        }
+    }
+    // check for APETAG
+    let mut buf = [0; 8];
+    if let Some(off) = end {
+        br.seek(SeekFrom::Start(off - 32)).unwrap();
+    } else {
+        br.seek(SeekFrom::End(-32)).unwrap();
+    }
+    let off = br.tell();
+    br.read_buf(&mut buf).unwrap();
+    if &buf == b"APETAGEX" {
+        let ver     = br.read_u32le().unwrap();
+        let size    = u64::from(br.read_u32le().unwrap());
+        let _items  = br.read_u32le().unwrap();
+        let flags   = br.read_u32le().unwrap();
+        if ver == 1000 || (flags & 0x80000000) == 0 {
+            end = Some(off - size + 32);
+        } else {
+            end = Some(off - size);
+        }
+    }
+    // check for MusicMatch tag
+    let ret = if let Some(endpos) = end {
+            br.seek(SeekFrom::Start(endpos - 0x30))
+        } else {
+            br.seek(SeekFrom::End(-0x30))
+        };
+    if ret.is_ok() && br.tell() > (0x2000 - 0x30) {
+        let mut buf = [0; 19];
+        br.peek_buf(&mut buf).unwrap();
+        if &buf == b"Brava Software Inc." {
+            br.seek(SeekFrom::Current(-20)).unwrap();
+            let mut mm_start = u64::from(br.read_u32le().unwrap());
+            if mm_start > 4 && mm_start + 0x2000 <= br.tell() {
+                let diff = (br.tell() - mm_start) & 3;
+                if diff != 0 {
+                    mm_start -= 4 - diff;
+                }
+                end = Some(mm_start);
+            }
+        }
+    }
+
+    (is_raw, start, end)
+}
diff --git a/nihav-hlblocks/src/imgseqdec.rs b/nihav-hlblocks/src/imgseqdec.rs
new file mode 100644 (file)
index 0000000..a580655
--- /dev/null
@@ -0,0 +1,351 @@
+//! Image sequence demuxer
+//!
+//! This module offers a functionality of representing an image sequence with an object having `Demuxer`-like interface.
+use nihav_core::frame::*;
+use nihav_core::demuxers::*;
+use std::fs::File;
+use std::io::BufReader;
+use std::io::Read;
+
+struct TemplateName {
+    prefix:         String,
+    pad_size:       usize,
+    suffix:         String,
+    single:         bool,
+}
+
+trait Deescape {
+    fn deescape(&mut self);
+}
+
+impl Deescape for String {
+    fn deescape(&mut self) {
+        while let Some(idx) = self.find("%%") {
+            self.remove(idx + 1);
+        }
+    }
+}
+
+impl TemplateName {
+    fn new(name: &str) -> Self {
+        let mut off = 0;
+        let mut tmpl_start = 0;
+        let mut tmpl_end = 0;
+        let mut pad_size = 0;
+        'parse_loop:
+        while let Some(idx) = name[off..].find('%') {
+            let idx = idx + off;
+            if idx + 1 == name.len() {
+                break;
+            }
+            if name[idx + 1..].starts_with('%') { // escape, skip it
+                off += 1;
+            }
+            if name[idx + 1..].starts_with('0') {
+                if let Some(end_idx) = name[idx + 2..].find('d') {
+                    if let Ok(val) = name[idx + 2..][..end_idx].parse::<usize>() {
+                        if val <= 32 {
+                            tmpl_start = idx;
+                            pad_size = val;
+                            tmpl_end = idx + 2 + end_idx + 1;
+                        }
+                    }
+                    break 'parse_loop;
+                }
+            }
+            if name[idx + 1..].starts_with('d') {
+                tmpl_start = idx;
+                tmpl_end = idx + 2;
+                break;
+            }
+            off += idx;
+        }
+
+        if tmpl_end == 0 {
+            let mut prefix = name.to_owned();
+            prefix.deescape();
+            Self {
+                prefix,
+                pad_size:   0,
+                suffix:     String::new(),
+                single:     true,
+            }
+        } else {
+            let mut prefix = name[..tmpl_start].to_string();
+            prefix.deescape();
+            let mut suffix = name[tmpl_end..].to_string();
+            suffix.deescape();
+            Self {
+                prefix, suffix, pad_size,
+                single: false,
+            }
+        }
+    }
+    fn format<T: Sized+ToString>(&self, id: T) -> String {
+        let mut number = id.to_string();
+        while number.len() < self.pad_size {
+            number.insert(0, '0');
+        }
+        let mut fname = String::with_capacity(self.prefix.len() + number.len() + self.suffix.len());
+        fname.push_str(&self.prefix);
+        fname.push_str(&number);
+        fname.push_str(&self.suffix);
+        fname
+    }
+}
+
+/// Image sequence demuxer
+///
+/// Use [`ImgSeqDemuxerCreator`] to create an instance of it.
+///
+/// [`ImgSeqDemuxerCreator`]: ./struct.ImgSeqDemuxerCreator.html
+pub struct ImgSeqDemuxer {
+    pub stream:     NAStreamRef,
+    pub sm:         StreamManager,
+    cur_frame:      u64,
+    template:       TemplateName,
+    pgmyuv:         bool,
+}
+
+impl ImgSeqDemuxer {
+    fn new(stream: NAStreamRef, cur_frame: u64, template: TemplateName, pgmyuv: bool) -> Self {
+        let mut sm = StreamManager::new();
+        sm.add_stream_ref(stream.clone());
+        Self {
+            stream, sm, cur_frame, template, pgmyuv,
+        }
+    }
+    /// Seeks to the requested time if possible.
+    pub fn seek(&mut self, time: NATimePoint) -> DemuxerResult<()> {
+        self.cur_frame = match time {
+                NATimePoint::None => return Ok(()),
+                NATimePoint::Milliseconds(ms) => NATimeInfo::rescale_ts(ms, 1, 1000, self.stream.tb_num, self.stream.tb_den),
+                NATimePoint::PTS(pts) => pts,
+            };
+        Ok(())
+    }
+    /// Demuxes a packet.
+    pub fn get_frame(&mut self) -> DemuxerResult<NAPacket> {
+        if self.cur_frame > 0 && self.template.single {
+            return Err(DemuxerError::EOF);
+        }
+        let fname = self.template.format(self.cur_frame);
+        if let Ok(file) = File::open(fname.as_str()) {
+            let mut file = BufReader::new(file);
+            let vinfo = read_pnm_header(&mut file, self.pgmyuv)?;
+            let pkt_size = if vinfo.format.model.is_yuv() && vinfo.format.components == 3 {
+                    vinfo.width * (vinfo.height * 3 / 2) * if vinfo.format.get_max_depth() > 8 { 2 } else { 1 }
+                } else {
+                    vinfo.width * vinfo.height * usize::from(vinfo.format.components)
+                };
+            let mut buf = vec![0; pkt_size];
+            file.read_exact(&mut buf).map_err(|_| DemuxerError::IOError)?;
+            let ts = NATimeInfo::new(Some(self.cur_frame), None, None, self.stream.tb_num, self.stream.tb_den);
+            let pkt = NAPacket::new(self.stream.clone(), ts, true, buf);
+            self.cur_frame += 1;
+            Ok(pkt)
+        } else {
+            Err(DemuxerError::EOF)
+        }
+    }
+}
+
+impl NAOptionHandler for ImgSeqDemuxer {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] }
+    fn set_options(&mut self, _options: &[NAOption]) {}
+    fn query_option_value(&self, _name: &str) -> Option<NAValue> { None }
+}
+
+/// Helper object that creates image sequence demuxer
+///
+/// By default it will try to open images using the provided simplified `printf`-like template e.g. `file%d.ppm` will be expanded into `file0.ppm`, `file1.ppm`, `file2.ppm` ... and `img%03d.pnm` will be expanded into `img000.pnm`, `img001.pnm` etc.
+/// Please note that passing single image names like `picture.pgm` is fine, also if `img000.ppm` is not found but `img001.ppm` is then decoding will start from it. You can set start image in the sequence with `start` option.
+///
+/// Other useful options are:
+/// * `tb_num`/`tb_den` pair allows to set custom timebase for the sequence
+/// * `pgmyuv` -- tells the demuxer that input PGM images are not greyscale images but rather YUV 4:2:0 data with chroma planes next to each other and below luma plane data.
+pub struct ImgSeqDemuxerCreator<'a> {
+    name:       &'a str,
+    start:      Option<u64>,
+    pgmyuv:     bool,
+    tb_num:     u32,
+    tb_den:     u32,
+}
+
+impl<'a> ImgSeqDemuxerCreator<'a> {
+    /// Creates a new instance of `ImgSeqDemuxerCreator`, allowing to set options before actual demuxer is created.
+    pub fn new(name: &'a str) -> Self {
+        Self {
+            name,
+            start:      None,
+            pgmyuv:     false,
+            tb_num:     1,
+            tb_den:     25,
+        }
+    }
+    /// Tries to create new image sequence demuxer.
+    ///
+    /// Note: for setting stream properties the first image in the sequence is checked.
+    pub fn open(&mut self) -> DemuxerResult<ImgSeqDemuxer> {
+        let template = TemplateName::new(self.name);
+
+        let fname = template.format(self.start.unwrap_or(0));
+
+        if let Ok(file) = File::open(fname.as_str()) {
+            let mut file = BufReader::new(file);
+            let vinfo = read_pnm_header(&mut file, self.pgmyuv)?;
+            let cinfo = NACodecInfo::new("rawvideo", NACodecTypeInfo::Video(vinfo), None);
+            let stream = NAStream::new(StreamType::Video, 0, cinfo, self.tb_num, self.tb_den, 0).into_ref();
+            return Ok(ImgSeqDemuxer::new(stream, self.start.unwrap_or(0), template, self.pgmyuv));
+        }
+
+        // if start is not given, try also starting from one
+        if self.start.is_none() {
+            let new_start = 1;
+            let fname = template.format(new_start);
+            if let Ok(file) = File::open(fname.as_str()) {
+                let mut file = BufReader::new(file);
+                let vinfo = read_pnm_header(&mut file, self.pgmyuv)?;
+                let cinfo = NACodecInfo::new("rawvideo", NACodecTypeInfo::Video(vinfo), None);
+                let stream = NAStream::new(StreamType::Video, 0, cinfo, self.tb_num, self.tb_den, 0).into_ref();
+                return Ok(ImgSeqDemuxer::new(stream, new_start, template, self.pgmyuv));
+            }
+        }
+
+        Err(DemuxerError::NoSuchInput)
+    }
+}
+
+const IMGSEQ_OPTIONS: &[NAOptionDefinition] = &[
+    NAOptionDefinition {
+        name: "start", description: "start frame number",
+        opt_type: NAOptionDefinitionType::Int(Some(0), None) },
+    NAOptionDefinition {
+        name: "pgmyuv", description: "Input is in PGMYUV format",
+        opt_type: NAOptionDefinitionType::Bool },
+    NAOptionDefinition {
+        name: "tb_num", description: "timebase numerator",
+        opt_type: NAOptionDefinitionType::Int(Some(1), Some(1000000)) },
+    NAOptionDefinition {
+        name: "tb_den", description: "timebase denominator",
+        opt_type: NAOptionDefinitionType::Int(Some(1), Some(1000000)) },
+];
+
+impl<'a> NAOptionHandler for ImgSeqDemuxerCreator<'a> {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { IMGSEQ_OPTIONS }
+    fn set_options(&mut self, options: &[NAOption]) {
+        for option in options.iter() {
+            for opt_def in IMGSEQ_OPTIONS.iter() {
+                if opt_def.check(option).is_ok() {
+                    match (option.name, &option.value) {
+                        ("start", NAValue::Int(intval)) => {
+                            self.start = Some(*intval as u64);
+                        },
+                        ("pgmyuv", NAValue::Bool(bval)) => {
+                            self.pgmyuv = *bval;
+                        },
+                        ("tb_num", NAValue::Int(intval)) => {
+                            self.tb_num = *intval as u32;
+                        },
+                        ("tb_den", NAValue::Int(intval)) => {
+                            self.tb_den = *intval as u32;
+                        },
+                        _ => {},
+                    }
+                }
+            }
+        }
+    }
+    fn query_option_value(&self, name: &str) -> Option<NAValue> {
+        match name {
+            "start" => Some(NAValue::Int(self.start.unwrap_or(0) as i64)),
+            "pgmyuv" => Some(NAValue::Bool(self.pgmyuv)),
+            "tb_num" => Some(NAValue::Int(self.tb_num as i64)),
+            "tb_den" => Some(NAValue::Int(self.tb_den as i64)),
+            _ => None,
+        }
+    }
+}
+
+fn read_pnm_header(file: &mut BufReader<File>, pgmyuv: bool) -> DemuxerResult<NAVideoInfo> {
+    let mut br = FileReader::new_read(file);
+
+    let mut magic = [0; 2];
+    br.read_buf(&mut magic)?;
+    if magic[0] != b'P' { return Err(DemuxerError::InvalidData); }
+    match magic[1] {
+        b'4' | // PBM, PBM ASCII
+        b'1' => return Err(DemuxerError::NotImplemented),
+        b'5' => { // PGM
+        },
+        b'2' => return Err(DemuxerError::NotImplemented), // PGM ASCII
+        b'6' => { // PPM
+        },
+        b'3' => return Err(DemuxerError::NotImplemented), // PPM ASCII
+        _ => return Err(DemuxerError::InvalidData),
+    };
+    if br.read_byte()? != b'\n' { return Err(DemuxerError::InvalidData); }
+    let w = read_number(&mut br)?;
+    let h = read_number(&mut br)?;
+    let maxval = if matches!(magic[1], b'4' | b'1') { 1 } else { read_number(&mut br)? };
+    if maxval > 65535 || (maxval & (maxval + 1)) != 0 { return Err(DemuxerError::InvalidData); }
+    let bits = maxval.count_ones() as u8;
+
+    let mut vinfo = NAVideoInfo::new(w, h, false, RGB24_FORMAT);
+    match magic[1] {
+        b'5' | b'2' if !pgmyuv => {
+            vinfo.format = NAPixelFormaton {
+                    model: ColorModel::YUV(YUVSubmodel::YUVJ),
+                    components: 1,
+                    comp_info: [Some(NAPixelChromaton{h_ss: 0, v_ss: 0, packed: false, depth: 8, shift: 0, comp_offs: 0, next_elem: 1}), None, None, None, None],
+                    elem_size: 1,
+                    be: true,
+                    alpha: false,
+                    palette: false,
+                };
+            vinfo.bits = bits;
+        },
+        b'5' | b'2' => {
+            if ((w & 1) != 0) || ((h % 3) != 0) { return Err(DemuxerError::InvalidData); }
+            vinfo.format = YUV420_FORMAT;
+            vinfo.height = h * 2 / 3;
+            vinfo.bits = bits * 3 / 2;
+        },
+        b'6' | b'3' => {
+            vinfo.format = RGB24_FORMAT;
+            vinfo.bits = bits;
+        },
+        _ => unreachable!(),
+    };
+    if bits != 8 {
+        for chr in vinfo.format.comp_info.iter_mut().flatten() {
+            chr.depth = bits;
+            if bits > 8 {
+                chr.next_elem = 2;
+            }
+        }
+        if bits > 8 {
+            vinfo.format.elem_size <<= 1;
+        }
+    }
+
+    Ok(vinfo)
+}
+
+fn read_number(br: &mut dyn ByteIO) -> DemuxerResult<usize> {
+    let mut val = 0;
+    loop {
+        let c = br.read_byte()?;
+        match c {
+            b'0'..=b'9' => {
+                if val > 1048576 {
+                    return Err(DemuxerError::InvalidData);
+                }
+                val = val * 10 + usize::from(c - b'0');
+            },
+            b' ' | b'\n' => break,
+            _ => return Err(DemuxerError::InvalidData),
+        };
+    }
+    Ok(val)
+}
diff --git a/nihav-hlblocks/src/lib.rs b/nihav-hlblocks/src/lib.rs
new file mode 100644 (file)
index 0000000..e440eb4
--- /dev/null
@@ -0,0 +1,8 @@
+//! Common blocks for building high-level tools.
+
+#[cfg(feature="demuxer")]
+pub mod demux;
+
+#[cfg(feature="imgseq_dec")]
+pub mod imgseqdec;
+