From 8e421d193862ccd8a949ae152ff994d502ff67b3 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Sat, 28 Mar 2026 18:26:56 +0100 Subject: [PATCH] (very very imperfect) MOV muxer --- nihav-commonfmt/Cargo.toml | 3 +- nihav-commonfmt/src/muxers/mod.rs | 4 + nihav-commonfmt/src/muxers/mov/audiotrack.rs | 101 +++ nihav-commonfmt/src/muxers/mov/mod.rs | 580 ++++++++++++++++++ .../src/muxers/mov/rawaudiotrack.rs | 130 ++++ nihav-commonfmt/src/muxers/mov/videotrack.rs | 126 ++++ 6 files changed, 943 insertions(+), 1 deletion(-) create mode 100644 nihav-commonfmt/src/muxers/mov/audiotrack.rs create mode 100644 nihav-commonfmt/src/muxers/mov/mod.rs create mode 100644 nihav-commonfmt/src/muxers/mov/rawaudiotrack.rs create mode 100644 nihav-commonfmt/src/muxers/mov/videotrack.rs diff --git a/nihav-commonfmt/Cargo.toml b/nihav-commonfmt/Cargo.toml index 59afe90..854495c 100644 --- a/nihav-commonfmt/Cargo.toml +++ b/nihav-commonfmt/Cargo.toml @@ -29,9 +29,10 @@ demuxer_gif = ["demuxers"] demuxer_mov = ["demuxers"] demuxer_wav = ["demuxers"] demuxer_y4m = ["demuxers"] -all_muxers = ["muxer_avi", "muxer_gif", "muxer_wav", "muxer_y4m"] +all_muxers = ["muxer_avi", "muxer_gif", "muxer_mov", "muxer_wav", "muxer_y4m"] muxer_avi = ["muxers"] muxer_gif = ["muxers"] +muxer_mov = ["muxers"] muxer_wav = ["muxers"] muxer_y4m = ["muxers"] diff --git a/nihav-commonfmt/src/muxers/mod.rs b/nihav-commonfmt/src/muxers/mod.rs index 7aa19bd..f8bfcf3 100644 --- a/nihav-commonfmt/src/muxers/mod.rs +++ b/nihav-commonfmt/src/muxers/mod.rs @@ -4,6 +4,8 @@ use nihav_core::muxers::*; mod avi; #[cfg(feature="muxer_gif")] mod gif; +#[cfg(feature="muxer_mov")] +mod mov; #[cfg(feature="muxer_wav")] mod wav; #[cfg(feature="muxer_y4m")] @@ -14,6 +16,8 @@ const MUXERS: &[&dyn MuxerCreator] = &[ &avi::AVIMuxerCreator {}, #[cfg(feature="muxer_gif")] &gif::GIFMuxerCreator {}, +#[cfg(feature="muxer_mov")] + &mov::MovMuxerCreator {}, #[cfg(feature="muxer_wav")] &wav::WAVMuxerCreator {}, #[cfg(feature="muxer_y4m")] diff --git a/nihav-commonfmt/src/muxers/mov/audiotrack.rs b/nihav-commonfmt/src/muxers/mov/audiotrack.rs new file mode 100644 index 0000000..04cf078 --- /dev/null +++ b/nihav-commonfmt/src/muxers/mov/audiotrack.rs @@ -0,0 +1,101 @@ +use nihav_core::muxers::*; +use nihav_registry::register::*; + +use super::*; + +pub struct AudioTrackHandler { + fcc: [u8; 4], + ainfo: NAAudioInfo, + edata: Option>>, + pts: u64, + last_ts: u64, + frameno: u32, + frm_sz: usize, +} + +impl AudioTrackHandler { + pub fn new(strm: NAStreamRef) -> MuxerResult { + let cname = strm.get_info().get_name(); + let ainfo = strm.get_info().get_properties().get_audio_info().ok_or(MuxerError::UnsupportedFormat)?; + let fcc = if let Some(fcc) = find_mov_audio_fourcc(cname) { + fcc + } else if let Some(tcc) = find_wav_twocc(cname) { + [b'm', b's', (tcc >> 8) as u8, tcc as u8] + } else { + return Err(MuxerError::UnsupportedFormat); + }; + let edata = strm.get_info().get_extradata(); + Ok(Self{ + fcc, ainfo, edata, + pts: 0, + last_ts: 0, + frameno: 1, + frm_sz: 0, + }) + } +} + +impl TrackHandler for AudioTrackHandler { + fn write_chunk(&mut self, bw: &mut dyn ByteIO, ca: &mut ChunkAccount, pkt: NAPacket) -> MuxerResult<()> { + let src_pts = pkt.ts.pts.ok_or(MuxerError::InvalidData)?; + let src = pkt.get_buffer(); + if self.frameno == 1 { + self.frm_sz = src.len(); + } + if self.frm_sz != src.len() { + self.frm_sz = 0; + } + let offset = bw.tell(); + let pts = NATimeInfo::rescale_ts(src_pts, pkt.ts.tb_num, pkt.ts.tb_den, 1, self.ainfo.sample_rate); + ca.add_pts(pts); + ca.add_offset(offset); + ca.add_size(src.len()); + if pkt.is_keyframe() { + ca.add_keyframe(self.frameno); + } + self.frameno += 1; + self.pts = pts + NATimeInfo::rescale_ts(pkt.ts.duration.unwrap_or(1), pkt.ts.tb_num, pkt.ts.tb_den, 1, self.ainfo.sample_rate); + self.last_ts = pts; + bw.write_buf(&src)?; + Ok(()) + } + fn flush_chunks(&mut self, _bw: &mut dyn ByteIO, _ca: &mut ChunkAccount) -> MuxerResult<()> { Ok(()) } + fn get_time_info(&self) -> (u64, u32) { (self.pts, self.ainfo.sample_rate) } + fn get_dimensions(&self) -> (usize, usize) { (0, 0) } + fn write_media_info(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let smhd = AtomMarker::start(bw, b"smhd")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u16be(0)?; // balance + bw.write_u16be(0)?; // reserved + smhd.end(bw)?; + Ok(()) + } + fn write_descriptor(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let codec = AtomMarker::start(bw, &self.fcc)?; + bw.write_buf(&[0; 6])?; + bw.write_u16be(1)?; // data reference index + bw.write_u16be(1)?; // version + bw.write_u16be(0)?; // revision + bw.write_u32be(0)?; // vendor + bw.write_u16be(u16::from(self.ainfo.channels))?; + bw.write_u16be(u16::from(self.ainfo.format.bits))?; + bw.write_u16be(0xFFFE)?; // compression ID + bw.write_u16be(0)?; // packet size + bw.write_u32be(self.ainfo.sample_rate << 16)?; + bw.write_u32be(0)?; // samples per packet + bw.write_u32be(self.frm_sz as u32)?; // bytes per packet + bw.write_u32be(0)?; // bytes per frame + bw.write_u32be(0)?; // bytes per sample + if let Some(edata) = &self.edata { + bw.write_u32be(edata.len() as u32 + 20)?; + bw.write_buf(b"wave")?; + bw.write_u32be(12)?; + bw.write_buf(b"frma")?; + bw.write_buf(&self.fcc)?; + bw.write_buf(edata)?; + } + codec.end(bw)?; + Ok(()) + } +} diff --git a/nihav-commonfmt/src/muxers/mov/mod.rs b/nihav-commonfmt/src/muxers/mov/mod.rs new file mode 100644 index 0000000..467cd9a --- /dev/null +++ b/nihav-commonfmt/src/muxers/mov/mod.rs @@ -0,0 +1,580 @@ +use std::convert::TryInto; + +use nihav_core::muxers::*; + +mod audiotrack; +use audiotrack::*; +mod rawaudiotrack; +use rawaudiotrack::*; +mod videotrack; +use videotrack::*; + +const TIMESCALE: u32 = 1000; + +trait MovWrite { + fn write_pas_str(&mut self, pas_str: &[u8]) -> ByteIOResult<()>; +} + +impl MovWrite for T { + fn write_pas_str(&mut self, pas_str: &[u8]) -> ByteIOResult<()> { + self.write_byte(pas_str.len() as u8)?; + self.write_buf(pas_str)?; + Ok(()) + } +} + +fn write_matrix_structure(bw: &mut dyn ByteIO) -> MuxerResult<()> { + bw.write_u32be(0x00010000)?; + bw.write_u32be(0)?; + bw.write_u32be(0)?; + bw.write_u32be(0)?; + bw.write_u32be(0x00010000)?; + bw.write_u32be(0)?; + bw.write_u32be(0)?; + bw.write_u32be(0)?; + bw.write_u32be(0x40000000)?; + Ok(()) +} + +struct AtomMarker { + pos: u64, + name: [u8; 4], + done: bool, +} + +impl AtomMarker { + fn start(bw: &mut dyn ByteIO, tag: &[u8; 4]) -> MuxerResult { + let pos = bw.tell(); + bw.write_u32be(0)?; + bw.write_buf(tag)?; + Ok(Self{ pos, name: *tag, done: false }) + } + fn end(mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let cur_pos = bw.tell(); + if cur_pos < self.pos + 8 { + return Err(MuxerError::InvalidData); + } + bw.seek(SeekFrom::Start(self.pos))?; + bw.write_u32be((cur_pos - self.pos) as u32)?; + bw.seek(SeekFrom::Start(cur_pos))?; + self.done = true; + Ok(()) + } +} + +impl Drop for AtomMarker { + fn drop(&mut self) { + if !self.done { + println!("Unclosed marker '{}{}{}{}'", self.name[0] as char, self.name[1] as char, self.name[2] as char, self.name[3] as char); + } + } +} + +#[derive(Default)] +struct ChunkAccount { + offsets: Vec, + offsets64: Vec, + sizes: Vec, + cmap: Vec, + pts: Vec, + keyframes: Vec, +} + +impl ChunkAccount { + fn new() -> Self { Self::default() } + fn is_empty(&self) -> bool { self.offsets.is_empty() && self.offsets64.is_empty() } + + fn add_size(&mut self, size: usize) { + self.sizes.push(size as u32); + } + fn add_keyframe(&mut self, chunk: u32) { + self.keyframes.push(chunk); + } + fn add_pts(&mut self, pts: u64) { + self.pts.push(pts); + } + fn add_offset(&mut self, offset: u64) { + if let Ok(off32) = offset.try_into() { + self.offsets.push(off32); + } else { + if !self.offsets.is_empty() { + self.offsets64.reserve(self.offsets.len() + 1); + for &off32 in self.offsets.iter() { + self.offsets64.push(u64::from(off32)); + } + self.offsets = Vec::new(); + } + self.offsets64.push(offset); + } + } + fn add_chunk_samps(&mut self, count: usize) { + self.cmap.push(count as u32); + } + + fn write_stts(&mut self, bw: &mut dyn ByteIO, raw_audio: bool) -> MuxerResult<()> { + if !raw_audio { + let mut nentries = 1; + let mut prev_delta = self.pts[1] - self.pts[0]; + for w in self.pts.windows(2).skip(1) { + let delta = w[1] - w[0]; + if delta != prev_delta { + nentries += 1; + prev_delta = delta; + } + } + bw.write_u32be(nentries as u32)?; + let mut prev_delta = self.pts[1] - self.pts[0]; + let mut run = 1; + for w in self.pts.windows(2).skip(1) { + let delta = w[1] - w[0]; + if delta != prev_delta { + bw.write_u32be(run)?; + bw.write_u32be(prev_delta as u32)?; + run = 1; + prev_delta = delta; + } else { + run += 1; + } + } + bw.write_u32be(run + 1)?; + bw.write_u32be(prev_delta as u32)?; + } else { + bw.write_u32be(1)?; + bw.write_u32be((*self.pts.last().unwrap_or(&0)) as u32)?; + bw.write_u32be(1)?; + } + Ok(()) + } + fn write_stsc(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + if self.cmap.is_empty() { + bw.write_u32be(1)?; // number of entries + bw.write_u32be(1)?; // first chunk + bw.write_u32be(1)?; // samples per chunk + bw.write_u32be(1)?; // sample description ID + } else { + bw.write_u32be(self.cmap.len() as u32)?; + for (i, &entry) in self.cmap.iter().enumerate() { + bw.write_u32be(i as u32 + 1)?; + bw.write_u32be(entry)?; // samples per chunk + bw.write_u32be(1)?; // sample description ID + } + } + Ok(()) + } + fn write_stsz(&mut self, bw: &mut dyn ByteIO, raw_audio: bool) -> MuxerResult<()> { + if !raw_audio { + bw.write_u32be(0)?; // sample size + bw.write_u32be(self.sizes.len() as u32)?; + bw.write_u32be_arr(&self.sizes)?; + } else { + bw.write_u32be(1)?; + let tot_size = self.sizes.iter().sum(); + bw.write_u32be(tot_size)?; + } + Ok(()) + } + fn write_chunk_offsets(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + if self.offsets64.is_empty() { + let stco = AtomMarker::start(bw, b"stco")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u32be(self.offsets.len() as u32)?; + bw.write_u32be_arr(&self.offsets)?; + stco.end(bw)?; + } else { + let co64 = AtomMarker::start(bw, b"co64")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + for &off in self.offsets64.iter() { + bw.write_u64be(off)?; + } + co64.end(bw)?; + } + Ok(()) + } + fn write_stss(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + bw.write_u32be(self.keyframes.len() as u32)?; + bw.write_u32be_arr(&self.keyframes)?; + Ok(()) + } +} + +trait TrackHandler { + fn write_chunk(&mut self, bw: &mut dyn ByteIO, ca: &mut ChunkAccount, pkt: NAPacket) -> MuxerResult<()>; + fn flush_chunks(&mut self, bw: &mut dyn ByteIO, ca: &mut ChunkAccount) -> MuxerResult<()>; + fn get_time_info(&self) -> (u64, u32); + fn get_dimensions(&self) -> (usize, usize); + fn write_media_info(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()>; + fn write_descriptor(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()>; +} + +struct Track { + id: u32, + mtype: StreamType, + handler: Box, + ca: ChunkAccount, + raw_audio: bool, +} + +impl Track { + fn write_trak(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let trak = AtomMarker::start(bw, b"trak")?; + + let tkhd = AtomMarker::start(bw, b"tkhd")?; + bw.write_byte(0)?; // version + bw.write_u24be(0xF)?; // flags (from LSB) - enabled, in movie, in preview, in poster + bw.write_u32be(0)?; // creation time + bw.write_u32be(0)?; // modification time + bw.write_u32be(self.id)?; + bw.write_u32be(0)?; // reserved + let (nduration, tbase) = self.handler.get_time_info(); + let duration = NATimeInfo::rescale_ts(nduration, 1, tbase, 1, TIMESCALE) as u32; + bw.write_u32be(duration)?; // duration + bw.write_buf(&[0; 8])?; // reserved + bw.write_u16be(0)?; // layer + bw.write_u16be(0)?; // alternate group + bw.write_u16be(if self.mtype != StreamType::Audio { 0 } else { 0x00FF })?; // volume + bw.write_u16be(0)?; // reserved + write_matrix_structure(bw)?; + let (width, height) = self.handler.get_dimensions(); + bw.write_u32be(width as u32)?; + bw.write_u32be(height as u32)?; + tkhd.end(bw)?; + + let mdia = AtomMarker::start(bw, b"mdia")?; + let mdhd = AtomMarker::start(bw, b"mdhd")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u32be(0)?; // creation time + bw.write_u32be(0)?; // modification time + let (duration, time_scale) = self.handler.get_time_info(); + bw.write_u32be(time_scale)?; + bw.write_u32be(duration as u32)?; + bw.write_u16be(0)?; // language + bw.write_u16be(0)?; // quality + mdhd.end(bw)?; + let hdlr = AtomMarker::start(bw, b"hdlr")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_buf(b"mhlr")?; // component type = media + let hd = match self.mtype { + StreamType::Video => b"vide", + StreamType::Audio => b"soun", + _ => unimplemented!(), + }; + bw.write_buf(hd)?; + bw.write_buf(b"appl")?; // component manufacturer + bw.write_u32be(0)?; // component flags + bw.write_u32be(0)?; // component flags mask + let comp_string = match self.mtype { + StreamType::Video => b"Video handler", + StreamType::Audio => b"Audio handler", + _ => unimplemented!(), + }; + bw.write_pas_str(comp_string)?; + hdlr.end(bw)?; + + let minf = AtomMarker::start(bw, b"minf")?; + self.handler.write_media_info(bw)?; + + let hdlr = AtomMarker::start(bw, b"hdlr")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_buf(b"dhlr")?; // component type = data handler + bw.write_buf(b"alis")?; + bw.write_buf(b"appl")?; // component manufacturer + bw.write_u32be(0)?; // component flags + bw.write_u32be(0)?; // component flags mask + bw.write_pas_str(b"Apple Alias Data Handler")?; + hdlr.end(bw)?; + + let dinf = AtomMarker::start(bw, b"dinf")?; + let dref = AtomMarker::start(bw, b"dref")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u32be(1)?; // number of entries + let alis = AtomMarker::start(bw, b"alis")?; + bw.write_byte(0)?; // version + bw.write_u24be(1)?; // flags + alis.end(bw)?; + dref.end(bw)?; + dinf.end(bw)?; + + let stbl = AtomMarker::start(bw, b"stbl")?; + + let stsd = AtomMarker::start(bw, b"stsd")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u32be(1)?; // number of entries + self.handler.write_descriptor(bw)?; + stsd.end(bw)?; + + let stts = AtomMarker::start(bw, b"stts")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + self.ca.write_stts(bw, self.raw_audio)?; + stts.end(bw)?; + + let stsc = AtomMarker::start(bw, b"stsc")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + self.ca.write_stsc(bw)?; + stsc.end(bw)?; + + let stsz = AtomMarker::start(bw, b"stsz")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + self.ca.write_stsz(bw, self.raw_audio)?; + stsz.end(bw)?; + + if !self.raw_audio && !self.ca.keyframes.is_empty() { + let stss = AtomMarker::start(bw, b"stss")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + self.ca.write_stss(bw)?; + stss.end(bw)?; + } + + self.ca.write_chunk_offsets(bw)?; + + stbl.end(bw)?; + minf.end(bw)?; + mdia.end(bw)?; + trak.end(bw)?; + Ok(()) + } +} + +struct QTMuxer<'a> { + bw: &'a mut dyn ByteIO, + tracks: Vec, +} + +impl<'a> QTMuxer<'a> { + fn new(bw: &'a mut dyn ByteIO) -> Self { + Self { + bw, + tracks: Vec::new(), + } + } +} + +impl<'a> MuxCore<'a> for QTMuxer<'a> { + fn create(&mut self, strmgr: &StreamManager) -> MuxerResult<()> { + if strmgr.get_num_streams() == 0 { + return Err(MuxerError::InvalidArgument); + } + if strmgr.get_num_streams() > 99 { + return Err(MuxerError::UnsupportedFormat); + } + let mut trk_id = 1; + for strm in strmgr.iter() { + let mtype = strm.get_media_type(); + match mtype { + StreamType::Video => { + self.tracks.push(Track { + id: trk_id, + mtype, + handler: Box::new(VideoTrackHandler::new(strm)?), + ca: ChunkAccount::new(), + raw_audio: false, + }); + trk_id += 1; + }, + StreamType::Audio => { + let raw_audio = is_raw_audio(strm.get_info().get_name()); + let handler: Box = if raw_audio { + Box::new(RawAudioTrackHandler::new(strm)?) + } else { + Box::new(AudioTrackHandler::new(strm)?) + }; + self.tracks.push(Track { + id: trk_id, + mtype, + handler, + ca: ChunkAccount::new(), + raw_audio, + }); + trk_id += 1; + }, + _ => return Err(MuxerError::UnsupportedFormat), + } + } + + let marker = AtomMarker::start(self.bw, b"wide")?; + marker.end(self.bw)?; + let marker = AtomMarker::start(self.bw, b"mdat")?; + marker.end(self.bw)?; + + Ok(()) + } + fn mux_frame(&mut self, _strmgr: &StreamManager, pkt: NAPacket) -> MuxerResult<()> { + if self.tracks.is_empty() { + return Err(MuxerError::NotCreated); + } + let stream = pkt.get_stream(); + let str_num = stream.get_num(); + if str_num >= self.tracks.len() { + return Err(MuxerError::UnsupportedFormat); + } + let trk = &mut self.tracks[str_num]; + + trk.handler.write_chunk(self.bw, &mut trk.ca, pkt)?; + + Ok(()) + } + fn flush(&mut self) -> MuxerResult<()> { + for trk in self.tracks.iter_mut() { + trk.handler.flush_chunks(self.bw, &mut trk.ca)?; + } + Ok(()) + } + fn end(&mut self) -> MuxerResult<()> { + if self.tracks.is_empty() || self.bw.tell() <= 16 { + return Err(MuxerError::NotCreated); + } + + self.flush()?; + + let mdat_size = self.bw.tell() - 8; + if mdat_size < (1 << 32) { + self.bw.seek(SeekFrom::Start(8))?; + self.bw.write_u32be(mdat_size as u32)?; + } else { + self.bw.seek(SeekFrom::Start(0))?; + self.bw.write_u32be(1)?; + self.bw.write_buf(b"mdat")?; + self.bw.write_u64be(mdat_size + 8)?; + } + self.bw.seek(SeekFrom::End(0))?; + + let moov = AtomMarker::start(self.bw, b"moov")?; + let mvhd = AtomMarker::start(self.bw, b"mvhd")?; + self.bw.write_byte(0)?; // version + self.bw.write_u24be(0)?; // flags + self.bw.write_u32be(0)?; // creation time + self.bw.write_u32be(0)?; // modification time + self.bw.write_u32be(TIMESCALE)?; + let mut duration = 0; + for trk in self.tracks.iter() { + let (nduration, tbase) = trk.handler.get_time_info(); + let dur = NATimeInfo::rescale_ts(nduration, 1, tbase, 1, TIMESCALE) as u32; + duration = duration.max(dur); + } + self.bw.write_u32be(duration)?; + self.bw.write_u32be(0x00010000)?; // preferred rate = 1.0 + self.bw.write_u16be(0x00FF)?; // preferred volume = 1.0 + self.bw.write_buf(&[0; 10])?; // reserved + write_matrix_structure(self.bw)?; + self.bw.write_u32be(0)?; // preview time + self.bw.write_u32be(0)?; // preview duration + self.bw.write_u32be(0)?; // poster time + self.bw.write_u32be(0)?; // selection time + self.bw.write_u32be(0)?; // selection duration + self.bw.write_u32be(0)?; // current time + self.bw.write_u32be(self.tracks.len() as u32 + 1)?; // next track ID + mvhd.end(self.bw)?; + + for trk in self.tracks.iter_mut() { + if !trk.ca.is_empty() { + trk.write_trak(self.bw)?; + } + } + + let udta = AtomMarker::start(self.bw, b"udta")?; + let writer = AtomMarker::start(self.bw, b"\xA9wrt")?; + let wr_str = b"Generated by NihAV muxer. Sorry about that!"; + self.bw.write_u16be(wr_str.len() as u16)?; + self.bw.write_u16be(0)?; // language code + self.bw.write_buf(wr_str)?; + writer.end(self.bw)?; + self.bw.write_u32be(0)?; // for historical reasons + udta.end(self.bw)?; + + moov.end(self.bw)?; + + self.bw.flush()?; + + Ok(()) + } +} + +impl<'a> NAOptionHandler for QTMuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub struct MovMuxerCreator {} + +impl MuxerCreator for MovMuxerCreator { + fn new_muxer<'a>(&self, bw: &'a mut dyn ByteIO) -> Box + 'a> { + Box::new(QTMuxer::new(bw)) + } + fn get_name(&self) -> &'static str { "mov" } + fn get_capabilities(&self) -> MuxerCapabilities { MuxerCapabilities::Universal } + fn get_quirks(&self) -> MuxerQuirks { MuxerQuirks(MUX_QUIRK_UNSYNC) } +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::demuxers::*; + use nihav_core::muxers::*; + use nihav_codec_support::test::enc_video::*; + use crate::*; + + #[test] + fn test_mov_muxer_video_only() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + // sample from King's Quest VI Macintosh edition + let dec_config = DecoderTestParams { + demuxer: "mov-macbin", + in_name: "assets/QT/Halfdome.bin", + limit: None, + stream_type: StreamType::None, + dmx_reg, dec_reg: RegisteredDecoders::new(), + }; + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + test_remuxing_md5(&dec_config, "mov", &mux_reg, + [0x842382db, 0xa6d23d85, 0x4fa746c2, 0x5c8ede5b]); + } + + #[test] + fn test_mov_muxer_audio_only() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + // sample from The Wonders of Electricity: An Adventure in Safety + let dec_config = DecoderTestParams { + demuxer: "mov-resfork", + in_name: "assets/QT/car.mov", + limit: None, + stream_type: StreamType::None, + dmx_reg, dec_reg: RegisteredDecoders::new(), + }; + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + test_remuxing_md5(&dec_config, "mov", &mux_reg, + [0xe6bf92a2, 0x6a9871e5, 0x98fc2dcb, 0xe032c119]); + } + + #[test] + fn test_mov_muxer_video_and_audio() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + // samples from https://samples.mplayerhq.hu/V-codecs/QTRLE/tidemo1-24bit-rle.mov + let dec_config = DecoderTestParams { + demuxer: "mov", + in_name: "assets/QT/tidemo1-24bit-rle.mov", + limit: None, + stream_type: StreamType::None, + dmx_reg, dec_reg: RegisteredDecoders::new(), + }; + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + test_remuxing_md5(&dec_config, "mov", &mux_reg, + [0xeb99a547, 0xd6c1a4ce, 0xc12d3451, 0x096639c5]); + } +} diff --git a/nihav-commonfmt/src/muxers/mov/rawaudiotrack.rs b/nihav-commonfmt/src/muxers/mov/rawaudiotrack.rs new file mode 100644 index 0000000..cc3278e --- /dev/null +++ b/nihav-commonfmt/src/muxers/mov/rawaudiotrack.rs @@ -0,0 +1,130 @@ +use nihav_core::muxers::*; + +use super::*; + +pub fn is_raw_audio(cname: &str) -> bool { + matches!(cname, + "pcm" | + "mace-3" | "mace-6" | + "ima-adpcm-qt" | + "ulaw" | "alaw" | + "ms\x00\x02" | "ms\x00\x11") +} + +pub struct RawAudioTrackHandler { + fcc: [u8; 4], + rate: u32, + bits: usize, + channels: usize, + sample: u64, +} + +impl RawAudioTrackHandler { + pub fn new(strm: NAStreamRef) -> MuxerResult { + let cname = strm.get_info().get_name(); + let ainfo = strm.get_info().get_properties().get_audio_info().ok_or(MuxerError::UnsupportedFormat)?; + let fcc = match cname { + "pcm" => { + match ainfo.format { + NASoniton { bits: 8, be: _, packed: _, planar: _, float: false, signed: false } => *b"raw ", + NASoniton { bits: 8, be: _, packed: _, planar: _, float: false, signed: true } => *b"twos", + NASoniton { bits: 16, be: true, packed: _, planar: _, float: false, signed: true } => *b"twos", + NASoniton { bits: 16, be: false, packed: _, planar: _, float: false, signed: true } => *b"sowt", + NASoniton { bits: 24, be: true, packed: _, planar: _, float: false, signed: true } => *b"in24", + NASoniton { bits: 32, be: true, packed: _, planar: _, float: false, signed: true } => *b"in32", + NASoniton { bits: 32, be: true, packed: _, planar: _, float: true, signed: _ } => *b"fl32", + NASoniton { bits: 64, be: true, packed: _, planar: _, float: true, signed: _ } => *b"fl64", + _ => return Err(MuxerError::UnsupportedFormat), + } + }, + "mace-3" => *b"MAC3", + "mace-6" => *b"MAC6", + "ima-adpcm-qt" => *b"ima4", + "alaw" => *b"alaw", + "ulaw" => *b"ulaw", + + "ms-adpcm" => return Err(MuxerError::UnsupportedFormat), //*b"ms\x00\x02", + "ima-adpcm-ms" => *b"ms\x00\x11", + _ => return Err(MuxerError::UnsupportedFormat), + }; + let rate = ainfo.sample_rate; + let bits = usize::from(ainfo.format.bits); + let channels = usize::from(ainfo.channels); + Ok(Self { fcc, rate, bits, channels, sample: 0 }) + } + + fn calculate_chunk_samples(&self, size: usize) -> usize { + match &self.fcc { + b"NONE" | b"raw " | b"twos" | b"sowt" | &[0, 0, 0, 0] => { + size * 8 / (self.bits * self.channels) + }, + b"ima4" => { + let nblocks = size / (34 * self.channels); + nblocks * 64 + }, + b"MAC3" => { + size * 3 / self.channels + }, + b"MAC6" => { + size * 6 / self.channels + }, + b"in24" => size / (3 * self.channels), + b"in32" | b"fl32" => size / (4 * self.channels), + b"fl64" => size / (8 * self.channels), + b"ulaw" | b"alaw" => size, + /*b"ms\x00\x02" => { //MS ADPCM + size / block_size * ((block_size / self.channels - 7) * 2 + 2) + },*/ + b"ms\x00\x11" => { //IMA ADPCM + (size / self.channels - 4) * 2 + 1 + }, + _ => unreachable!(), + } + } +} + +impl TrackHandler for RawAudioTrackHandler { + fn write_chunk(&mut self, bw: &mut dyn ByteIO, ca: &mut ChunkAccount, pkt: NAPacket) -> MuxerResult<()> { + let src = pkt.get_buffer(); + + let chunk_samples = self.calculate_chunk_samples(src.len()); + + let offset = bw.tell(); + ca.add_offset(offset); + ca.add_chunk_samps(chunk_samples); + self.sample += chunk_samples as u64; + bw.write_buf(&src)?; + Ok(()) + } + fn flush_chunks(&mut self, _bw: &mut dyn ByteIO, ca: &mut ChunkAccount) -> MuxerResult<()> { + ca.add_pts(self.sample); + Ok(()) + } + fn get_time_info(&self) -> (u64, u32) { (self.sample, self.rate) } + fn get_dimensions(&self) -> (usize, usize) { (0, 0) } + fn write_media_info(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let smhd = AtomMarker::start(bw, b"smhd")?; + bw.write_byte(0)?; // version + bw.write_u24be(0)?; // flags + bw.write_u16be(0)?; // balance + bw.write_u16be(0)?; // reserved + smhd.end(bw)?; + Ok(()) + } + fn write_descriptor(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let codec = AtomMarker::start(bw, &self.fcc)?; + bw.write_buf(&[0; 6])?; + bw.write_u16be(1)?; // data reference index + bw.write_u16be(0)?; // version + bw.write_u16be(0)?; // revision + bw.write_u32be(0)?; // vendor + bw.write_u16be(self.channels as u16)?; + bw.write_u16be(self.bits as u16)?; + bw.write_u16be(0)?; // compression ID + bw.write_u16be(0)?; // packet size + bw.write_u32be(self.rate << 16)?; + codec.end(bw)?; + + Ok(()) + } +} diff --git a/nihav-commonfmt/src/muxers/mov/videotrack.rs b/nihav-commonfmt/src/muxers/mov/videotrack.rs new file mode 100644 index 0000000..c59e9fd --- /dev/null +++ b/nihav-commonfmt/src/muxers/mov/videotrack.rs @@ -0,0 +1,126 @@ +use nihav_core::muxers::*; +use nihav_registry::register::*; + +use super::*; + +pub struct VideoTrackHandler { + fcc: [u8; 4], + vinfo: NAVideoInfo, + frameno: u32, + tb_num: u32, + tb_den: u32, + pts: u64, + edata: Option>>, +} + +impl VideoTrackHandler { + pub fn new(strm: NAStreamRef) -> MuxerResult { + let cname = strm.get_info().get_name(); + let fcc = find_mov_video_fourcc(cname).ok_or(MuxerError::UnsupportedFormat)?; + let vinfo = strm.get_info().get_properties().get_video_info().ok_or(MuxerError::UnsupportedFormat)?; + if cname == "rawvideo" { + match vinfo.format.to_short_string().ok_or(MuxerError::UnsupportedFormat)?.as_str() { + "rgb24" | "rgb555be" => {}, + other => { + println!("Raw format {other} is not supported!"); + return Err(MuxerError::UnsupportedFormat)?; + } + } + } + let (tb_num, tb_den) = strm.get_timebase(); + let edata = strm.get_info().get_extradata(); + + Ok(Self{ + fcc, vinfo, tb_num, tb_den, + frameno: 1, + pts: 0, + edata, + }) + } +} + +impl TrackHandler for VideoTrackHandler { + fn write_chunk(&mut self, bw: &mut dyn ByteIO, ca: &mut ChunkAccount, pkt: NAPacket) -> MuxerResult<()> { + let pts = pkt.ts.pts.ok_or(MuxerError::InvalidData)? * u64::from(self.tb_num); + let src = pkt.get_buffer(); + let offset = bw.tell(); + ca.add_pts(pts); + ca.add_offset(offset); + ca.add_size(src.len()); + if pkt.is_keyframe() { + ca.add_keyframe(self.frameno); + } + self.frameno += 1; + self.pts = pts + pkt.ts.duration.unwrap_or(1) * u64::from(self.tb_num); + bw.write_buf(&src)?; + Ok(()) + } + fn flush_chunks(&mut self, _bw: &mut dyn ByteIO, _ca: &mut ChunkAccount) -> MuxerResult<()> { Ok(()) } + fn get_time_info(&self) -> (u64, u32) { (self.pts * u64::from(self.tb_num), self.tb_den) } + fn get_dimensions(&self) -> (usize, usize) { + (self.vinfo.width, self.vinfo.height) + } + fn write_media_info(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let vmhd = AtomMarker::start(bw, b"vmhd")?; + bw.write_byte(0)?; // version + bw.write_u24be(1)?; // flags + bw.write_u16be(0x40)?; // graphics mode - dither copy + bw.write_u16be_arr(&[0x8000; 3])?; // opcolor + vmhd.end(bw)?; + Ok(()) + } + fn write_descriptor(&mut self, bw: &mut dyn ByteIO) -> MuxerResult<()> { + let codec = AtomMarker::start(bw, &self.fcc)?; + bw.write_buf(&[0; 6])?; + bw.write_u16be(1)?; // data reference index + bw.write_u16be(1)?; // version + bw.write_u16be(1)?; // revision + bw.write_buf(b"appl")?; // vendor + bw.write_u32be(0)?; // temporal quality + bw.write_u32be(0)?; // spatial quality + bw.write_u16be(self.vinfo.width as u16)?; + bw.write_u16be(self.vinfo.height as u16)?; + bw.write_u32be(72 << 16)?; // horizontal resolution + bw.write_u32be(72 << 16)?; // vertical resolution + bw.write_u32be(0)?; // data size + bw.write_u16be(1)?; // frame count + let name: &[u8] = match &self.fcc { + b"raw " | b"j420" => b"Raw video", + b"jpeg" => b"Motion JPEG", + b"mjpa" => b"Motion JPEG A", + b"mjpb" => b"Motion JPEG B", + b"cvid" => b"Cinepak", + b"smc " => b"Apple Graphics", + b"rpza" => b"Apple Video", + b"rle " => b"Apple Animation", + b"qdrw" => b"QuickDraw", + b"SVQ1" => b"Sorenson Video", + b"SVQ3" => b"Sorenson Video 3", + b"rt21" => b"Indeo 2", + b"IV31" | b"IV32" => b"Indeo 3", + b"MPAK" => b"MoviePak", + b"pgvv" => b"Radius Studio", + b"UCOD" => b"Clear Video", + b"VP30" | b"VP31" => b"Truemotion VP3", +//todo more + _ => b"Some unknown codec", + }; + bw.write_pas_str(name)?; + bw.write_buf(&[0; 32][..32 - name.len() - 1])?; + let mut bpp = match self.vinfo.format.get_total_depth() { + 15 | 16 => 16, + other_depth => other_depth + }; + if &self.fcc == b"j420" { + bpp = 12; + } + bw.write_u16be(u16::from(bpp))?; + bw.write_u16be(0xFFFF)?; // color table ID + if let Some(edata) = &self.edata { + bw.write_u32be(edata.len() as u32 + 4)?; + bw.write_buf(edata)?; + } + codec.end(bw)?; + Ok(()) + } +} -- 2.39.5