]> git.nihav.org Git - nihav.git/commitdiff
(very very imperfect) MOV muxer master
authorKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 28 Mar 2026 17:26:56 +0000 (18:26 +0100)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Sat, 28 Mar 2026 17:26:56 +0000 (18:26 +0100)
nihav-commonfmt/Cargo.toml
nihav-commonfmt/src/muxers/mod.rs
nihav-commonfmt/src/muxers/mov/audiotrack.rs [new file with mode: 0644]
nihav-commonfmt/src/muxers/mov/mod.rs [new file with mode: 0644]
nihav-commonfmt/src/muxers/mov/rawaudiotrack.rs [new file with mode: 0644]
nihav-commonfmt/src/muxers/mov/videotrack.rs [new file with mode: 0644]

index 59afe90d3b23f80784a5060b45fe50ca3de3d3ad..854495ca0245c1f5ca37e68a398976e44bfa872a 100644 (file)
@@ -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"]
 
index 7aa19bd28a378ae042864015dca658f00d7bff1d..f8bfcf34e8ea6c14c9e1defe86e3f4cf36550b5a 100644 (file)
@@ -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 (file)
index 0000000..04cf078
--- /dev/null
@@ -0,0 +1,101 @@
+use nihav_core::muxers::*;
+use nihav_registry::register::*;
+
+use super::*;
+
+pub struct AudioTrackHandler {
+    fcc:        [u8; 4],
+    ainfo:      NAAudioInfo,
+    edata:      Option<Arc<Vec<u8>>>,
+    pts:        u64,
+    last_ts:    u64,
+    frameno:    u32,
+    frm_sz:     usize,
+}
+
+impl AudioTrackHandler {
+    pub fn new(strm: NAStreamRef) -> MuxerResult<Self> {
+        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 (file)
index 0000000..467cd9a
--- /dev/null
@@ -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<T:?Sized + ByteIO> 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<Self> {
+        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<u32>,
+    offsets64:  Vec<u64>,
+    sizes:      Vec<u32>,
+    cmap:       Vec<u32>,
+    pts:        Vec<u64>,
+    keyframes:  Vec<u32>,
+}
+
+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<dyn TrackHandler>,
+    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<Track>,
+}
+
+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<dyn TrackHandler> = 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<NAValue> { None }
+}
+
+pub struct MovMuxerCreator {}
+
+impl MuxerCreator for MovMuxerCreator {
+    fn new_muxer<'a>(&self, bw: &'a mut dyn ByteIO) -> Box<dyn MuxCore<'a> + '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 (file)
index 0000000..cc3278e
--- /dev/null
@@ -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<Self> {
+        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 (file)
index 0000000..c59e9fd
--- /dev/null
@@ -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<Arc<Vec<u8>>>,
+}
+
+impl VideoTrackHandler {
+    pub fn new(strm: NAStreamRef) -> MuxerResult<Self> {
+        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(())
+    }
+}