From 4ea16cf36f0bc28d5b3d2f7cc29bcb1e62696d85 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Sat, 31 Jan 2026 15:43:38 +0100 Subject: [PATCH] introduce nihav_hlblocks, crate with common code for NihAV-based apps --- nihav-hlblocks/Cargo.toml | 17 + nihav-hlblocks/src/demux.rs | 556 ++++++++++++++++++++++++++++++++ nihav-hlblocks/src/imgseqdec.rs | 351 ++++++++++++++++++++ nihav-hlblocks/src/lib.rs | 8 + 4 files changed, 932 insertions(+) create mode 100644 nihav-hlblocks/Cargo.toml create mode 100644 nihav-hlblocks/src/demux.rs create mode 100644 nihav-hlblocks/src/imgseqdec.rs create mode 100644 nihav-hlblocks/src/lib.rs diff --git a/nihav-hlblocks/Cargo.toml b/nihav-hlblocks/Cargo.toml new file mode 100644 index 0000000..ee09e9a --- /dev/null +++ b/nihav-hlblocks/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nihav_hlblocks" +version = "0.1.0" +authors = ["Kostya Shishkov "] +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 index 0000000..707b14c --- /dev/null +++ b/nihav-hlblocks/src/demux.rs @@ -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, + br: &'a mut dyn ByteIO, + pts: u64, + seek: SeekIndex, +} + +impl<'a> RawStreamCtx<'a> { + fn new(stream: NAStreamRef, packetiser: Box, 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 { + 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>, + pkts: Vec>>, + eof: bool, +} + +/// Alias for input source +pub type ReaderBox = Box; + +/// 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>), + /// Raw stream demuxer with individual stream contexts + Raw(RawDemuxerState<'a>), + /// Elementary stream + RawStream(SBBox>), + #[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, 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, 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 { + 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 { + 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 { + 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) { + 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::() { + 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 index 0000000..a580655 --- /dev/null +++ b/nihav-hlblocks/src/imgseqdec.rs @@ -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::() { + 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(&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 { + 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 { 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, + 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 { + 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 { + 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, pgmyuv: bool) -> DemuxerResult { + 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 { + 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 index 0000000..e440eb4 --- /dev/null +++ b/nihav-hlblocks/src/lib.rs @@ -0,0 +1,8 @@ +//! Common blocks for building high-level tools. + +#[cfg(feature="demuxer")] +pub mod demux; + +#[cfg(feature="imgseq_dec")] +pub mod imgseqdec; + -- 2.39.5