diff --git a/examples/mp4dump.rs b/examples/mp4dump.rs index 6a97d9a0..597bb701 100644 --- a/examples/mp4dump.rs +++ b/examples/mp4dump.rs @@ -56,7 +56,9 @@ fn get_boxes(file: File) -> Result> { if let Some(mehd) = &mvex.mehd { boxes.push(build_box(mehd)); } - boxes.push(build_box(&mvex.trex)); + for trex in &mvex.trex { + boxes.push(build_box(trex)); + } } // trak. diff --git a/src/mp4box/mod.rs b/src/mp4box/mod.rs index 4bbdd410..9ba7791b 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -85,6 +85,7 @@ pub(crate) mod moov; pub(crate) mod mp4a; pub(crate) mod mvex; pub(crate) mod mvhd; +pub(crate) mod opus; pub(crate) mod smhd; pub(crate) mod stbl; pub(crate) mod stco; @@ -110,7 +111,7 @@ pub use avc1::Avc1Box; pub use co64::Co64Box; pub use ctts::CttsBox; pub use data::DataBox; -pub use dinf::DinfBox; +pub use dinf::*; pub use edts::EdtsBox; pub use elst::ElstBox; pub use emsg::EmsgBox; @@ -129,6 +130,7 @@ pub use moov::MoovBox; pub use mp4a::Mp4aBox; pub use mvex::MvexBox; pub use mvhd::MvhdBox; +pub use opus::*; pub use smhd::SmhdBox; pub use stbl::StblBox; pub use stco::StcoBox; @@ -139,7 +141,7 @@ pub use stsz::StszBox; pub use stts::SttsBox; pub use tfdt::TfdtBox; pub use tfhd::TfhdBox; -pub use tkhd::TkhdBox; +pub use tkhd::*; pub use traf::TrafBox; pub use trak::TrakBox; pub use trex::TrexBox; @@ -238,7 +240,9 @@ boxtype! { CovrBox => 0x636f7672, DescBox => 0x64657363, WideBox => 0x77696465, - WaveBox => 0x77617665 + WaveBox => 0x77617665, + DopsBox => 0x644F7073, + OpusBox => 0x4F707573 } pub trait Mp4Box: Sized { diff --git a/src/mp4box/moof.rs b/src/mp4box/moof.rs index 20c35653..1b1e9329 100644 --- a/src/mp4box/moof.rs +++ b/src/mp4box/moof.rs @@ -102,6 +102,6 @@ impl WriteBox<&mut W> for MoofBox { for traf in self.trafs.iter() { traf.write_box(writer)?; } - Ok(0) + Ok(size) } } diff --git a/src/mp4box/moov.rs b/src/mp4box/moov.rs index ac19381a..debdaeb8 100644 --- a/src/mp4box/moov.rs +++ b/src/mp4box/moov.rs @@ -29,15 +29,14 @@ impl MoovBox { pub fn get_size(&self) -> u64 { let mut size = HEADER_SIZE + self.mvhd.box_size(); + size += self.meta.as_ref().map(|x| x.box_size()).unwrap_or(0); + size += self.mvex.as_ref().map(|x| x.box_size()).unwrap_or(0); + for trak in self.traks.iter() { size += trak.box_size(); } - if let Some(meta) = &self.meta { - size += meta.box_size(); - } - if let Some(udta) = &self.udta { - size += udta.box_size(); - } + + size += self.udta.as_ref().map(|x| x.box_size()).unwrap_or(0); size } } @@ -131,16 +130,19 @@ impl WriteBox<&mut W> for MoovBox { BoxHeader::new(self.box_type(), size).write(writer)?; self.mvhd.write_box(writer)?; - for trak in self.traks.iter() { - trak.write_box(writer)?; - } if let Some(meta) = &self.meta { meta.write_box(writer)?; } + if let Some(mvex) = &self.mvex { + mvex.write_box(writer)?; + } + for trak in self.traks.iter() { + trak.write_box(writer)?; + } if let Some(udta) = &self.udta { udta.write_box(writer)?; } - Ok(0) + Ok(size) } } diff --git a/src/mp4box/mvex.rs b/src/mp4box/mvex.rs index 8be683b8..2941c4a6 100644 --- a/src/mp4box/mvex.rs +++ b/src/mp4box/mvex.rs @@ -7,16 +7,17 @@ use crate::mp4box::{mehd::MehdBox, trex::TrexBox}; #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] pub struct MvexBox { pub mehd: Option, - pub trex: TrexBox, + pub trex: Vec, } impl MvexBox { pub fn get_type(&self) -> BoxType { - BoxType::MdiaBox + BoxType::MvexBox } pub fn get_size(&self) -> u64 { - HEADER_SIZE + self.mehd.as_ref().map(|x| x.box_size()).unwrap_or(0) + self.trex.box_size() + let trex_sizes = self.trex.iter().fold(0, |acc, x| acc + x.box_size()); + HEADER_SIZE + self.mehd.as_ref().map(|x| x.box_size()).unwrap_or(0) + trex_sizes } } @@ -44,7 +45,7 @@ impl ReadBox<&mut R> for MvexBox { let start = box_start(reader)?; let mut mehd = None; - let mut trex = None; + let mut trex: Vec = Vec::new(); let mut current = reader.stream_position()?; let end = start + size; @@ -63,7 +64,7 @@ impl ReadBox<&mut R> for MvexBox { mehd = Some(MehdBox::read_box(reader, s)?); } BoxType::TrexBox => { - trex = Some(TrexBox::read_box(reader, s)?); + trex.push(TrexBox::read_box(reader, s)?); } _ => { // XXX warn!() @@ -74,16 +75,13 @@ impl ReadBox<&mut R> for MvexBox { current = reader.stream_position()?; } - if trex.is_none() { + if trex.is_empty() { return Err(Error::BoxNotFound(BoxType::TrexBox)); } skip_bytes_to(reader, start + size)?; - Ok(MvexBox { - mehd, - trex: trex.unwrap(), - }) + Ok(MvexBox { mehd, trex }) } } @@ -95,7 +93,9 @@ impl WriteBox<&mut W> for MvexBox { if let Some(mehd) = &self.mehd { mehd.write_box(writer)?; } - self.trex.write_box(writer)?; + for trex in &self.trex { + trex.write_box(writer)?; + } Ok(size) } diff --git a/src/mp4box/opus.rs b/src/mp4box/opus.rs new file mode 100644 index 00000000..438e40e7 --- /dev/null +++ b/src/mp4box/opus.rs @@ -0,0 +1,228 @@ +use crate::mp4box::*; +use crate::Mp4Box; +use serde::Serialize; + +// taken from the following sources +// - https://opus-codec.org/docs/opus_in_isobmff.html +// - chromium source code: box_definitions.h - OpusSpecificBox +// - async-mp4 crate: https://github.com/Wicpar/async-mp4/blob/master/src/mp4box/dops.rs + +// this OpusBox is a combination of the AudioSampleEntry box and OpusSpecificBox +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct OpusBox { + pub data_reference_index: u16, + pub channelcount: u16, + pub samplesize: u16, + + #[serde(with = "value_u32")] + pub samplerate: FixedPointU16, + pub dops: DopsBox, +} + +impl Mp4Box for OpusBox { + fn box_type(&self) -> BoxType { + BoxType::OpusBox + } + + fn box_size(&self) -> u64 { + // the +19 is for DopsBox + 36 + 19 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!("{self:?}")) + } +} + +impl ReadBox<&mut R> for OpusBox { + fn read_box(reader: &mut R, size: u64) -> Result { + box_start(reader)?; + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + reader.read_u64::()?; // reserved + let channelcount = reader.read_u16::()?; + let samplesize = reader.read_u16::()?; + reader.read_u32::()?; // reserved + let samplerate = reader.read_u32::()?; + + let num_read = 36; + if num_read > size { + return Err(Error::InvalidData("opus box is too small")); + } + let remaining = num_read.saturating_sub(size); + + // todo: if the size of DopsBox becomes variable, then the size of + // OpusBox will be needed to determine what size to pass to DopsBox::read_box() + let dops = DopsBox::read_box(reader, remaining)?; + + Ok(Self { + data_reference_index, + channelcount, + samplesize, + samplerate: FixedPointU16::new_raw(samplerate), + dops, + }) + } +} + +impl WriteBox<&mut W> for OpusBox { + fn write_box(&self, writer: &mut W) -> Result { + let mut written = 0; + written += BoxHeader::new(self.box_type(), self.box_size()).write(writer)?; + + writer.write_u32::(0)?; // reserved + written += 4; + writer.write_u16::(0)?; // reserved + written += 2; + writer.write_u16::(self.data_reference_index)?; + written += 2; + + writer.write_u64::(0)?; // reserved + written += 8; + writer.write_u16::(self.channelcount)?; + written += 2; + writer.write_u16::(self.samplesize)?; + written += 2; + writer.write_u32::(0)?; // reserved + written += 4; + writer.write_u32::(self.samplerate.raw_value())?; + written += 4; + + written += self.dops.write_box(writer)?; + + if written != self.box_size() { + Err(Error::InvalidData("written != box_size for OpusBox")) + } else { + Ok(written) + } + } +} + +// https://github.com/Wicpar/async-mp4/blob/master/src/mp4box/dops.rs +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DopsBox { + pub version: u8, + pub pre_skip: u16, + pub input_sample_rate: u32, + pub output_gain: i16, + pub channel_mapping_family: ChannelMappingFamily, +} + +impl Mp4Box for DopsBox { + fn box_type(&self) -> BoxType { + BoxType::DopsBox + } + + fn box_size(&self) -> u64 { + // if channel_mapping_family is updates to support more than 2 channels, + // box_size could change, depending on the channel mapping. + 19 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!("{self:?}")) + } +} + +impl ReadBox<&mut R> for DopsBox { + fn read_box(reader: &mut R, size: u64) -> Result { + if size < 19 { + return Err(Error::InvalidData("Dops box size too small")); + } + box_start(reader)?; + let version = reader.read_u8()?; + let num_channels = reader.read_u8()?; + let pre_skip = reader.read_u16::()?; + let input_sample_rate = reader.read_u32::()?; + let output_gain = reader.read_i16::()?; + + // todo: should this be used? + let _channel_mapping_family = reader.read_u8()?; + + Ok(DopsBox { + version, + pre_skip, + input_sample_rate, + output_gain, + channel_mapping_family: ChannelMappingFamily::Family0 { + stereo: num_channels == 2, + }, + }) + } +} + +// https://github.com/Wicpar/async-mp4/blob/master/src/mp4box/dops.rs +impl WriteBox<&mut W> for DopsBox { + fn write_box(&self, writer: &mut W) -> Result { + let mut written = 0; + written += BoxHeader::new(self.box_type(), self.box_size()).write(writer)?; + writer.write_u8(self.version)?; + written += 1; + + let num_channels = match self.channel_mapping_family { + ChannelMappingFamily::Family0 { stereo } => match stereo { + true => 2, + false => 1, + }, + }; + writer.write_u8(num_channels)?; + written += 1; + writer.write_u16::(self.pre_skip)?; + written += 2; + writer.write_u32::(self.input_sample_rate)?; + written += 4; + writer.write_i16::(self.output_gain)?; + written += 2; + + // channel mapping family 0 + writer.write_u8(0)?; + written += 1; + + // todo: StreamCount? CoupledCount? ChannelMapping? + + assert_eq!(written, self.box_size()); + Ok(written) + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum ChannelMappingFamily { + Family0 { stereo: bool }, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_opus_writer() { + let dops = DopsBox { + version: 0, + pre_skip: 1, + input_sample_rate: 2, + output_gain: 3, + channel_mapping_family: ChannelMappingFamily::Family0 { stereo: false }, + }; + + let opus = OpusBox { + data_reference_index: 1, + channelcount: 1, + samplesize: 2, + samplerate: FixedPointU16::new(48000), + dops, + }; + + let mut buffer = Vec::::new(); + opus.write_box(&mut buffer).expect("write_box failed"); + } +} diff --git a/src/mp4box/stsd.rs b/src/mp4box/stsd.rs index af947c6c..656742b3 100644 --- a/src/mp4box/stsd.rs +++ b/src/mp4box/stsd.rs @@ -25,8 +25,21 @@ pub struct StsdBox { #[serde(skip_serializing_if = "Option::is_none")] pub tx3g: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub opus: Option, } +// todo: all the Options in StsdBox may be better as an enumeration +// pub enum StsdChildBox { +// Avc1(Avc1Box), +// Hev1(Hev1Box), +// Vp09(Vp09Box), +// Mp4a(Mp4aBox), +// Tx3g(Tx3gBox), +// Opus(OpusBox), +// } + impl StsdBox { pub fn get_type(&self) -> BoxType { BoxType::StsdBox @@ -44,6 +57,8 @@ impl StsdBox { size += mp4a.box_size(); } else if let Some(ref tx3g) = self.tx3g { size += tx3g.box_size(); + } else if let Some(ref opus) = self.opus { + size += opus.box_size(); } size } @@ -81,6 +96,7 @@ impl ReadBox<&mut R> for StsdBox { let mut vp09 = None; let mut mp4a = None; let mut tx3g = None; + let mut opus = None; // Get box header. let header = BoxHeader::read(reader)?; @@ -107,6 +123,9 @@ impl ReadBox<&mut R> for StsdBox { BoxType::Tx3gBox => { tx3g = Some(Tx3gBox::read_box(reader, s)?); } + BoxType::OpusBox => { + opus = Some(OpusBox::read_box(reader, s)?); + } _ => {} } @@ -120,6 +139,7 @@ impl ReadBox<&mut R> for StsdBox { vp09, mp4a, tx3g, + opus, }) } } @@ -143,6 +163,8 @@ impl WriteBox<&mut W> for StsdBox { mp4a.write_box(writer)?; } else if let Some(ref tx3g) = self.tx3g { tx3g.write_box(writer)?; + } else if let Some(ref opus) = self.opus { + opus.write_box(writer)?; } Ok(size) diff --git a/src/mp4box/traf.rs b/src/mp4box/traf.rs index d53d713d..f9d6bfaa 100644 --- a/src/mp4box/traf.rs +++ b/src/mp4box/traf.rs @@ -19,12 +19,8 @@ impl TrafBox { pub fn get_size(&self) -> u64 { let mut size = HEADER_SIZE; size += self.tfhd.box_size(); - if let Some(ref tfdt) = self.tfdt { - size += tfdt.box_size(); - } - if let Some(ref trun) = self.trun { - size += trun.box_size(); - } + size += self.tfdt.as_ref().map(|x| x.box_size()).unwrap_or(0); + size += self.trun.as_ref().map(|x| x.box_size()).unwrap_or(0); size } } diff --git a/src/mp4box/trun.rs b/src/mp4box/trun.rs index efbb2b09..6f9f1d04 100644 --- a/src/mp4box/trun.rs +++ b/src/mp4box/trun.rs @@ -44,17 +44,18 @@ impl TrunBox { sum += 4; } if TrunBox::FLAG_SAMPLE_DURATION & self.flags > 0 { - sum += 4 * self.sample_count as u64; + sum += (4 * self.sample_durations.len()) as u64; } if TrunBox::FLAG_SAMPLE_SIZE & self.flags > 0 { - sum += 4 * self.sample_count as u64; + sum += (4 * self.sample_sizes.len()) as u64; } if TrunBox::FLAG_SAMPLE_FLAGS & self.flags > 0 { - sum += 4 * self.sample_count as u64; + sum += (4 * self.sample_flags.len()) as u64; } if TrunBox::FLAG_SAMPLE_CTS & self.flags > 0 { - sum += 4 * self.sample_count as u64; + sum += (4 * self.sample_cts.len()) as u64; } + sum } } @@ -185,9 +186,7 @@ impl WriteBox<&mut W> for TrunBox { if let Some(v) = self.first_sample_flags { writer.write_u32::(v)?; } - if self.sample_count != self.sample_sizes.len() as u32 { - return Err(Error::InvalidData("sample count out of sync")); - } + for i in 0..self.sample_count as usize { if TrunBox::FLAG_SAMPLE_DURATION & self.flags > 0 { writer.write_u32::(self.sample_durations[i])?; diff --git a/src/reader.rs b/src/reader.rs index e5ac2964..a1445836 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -97,18 +97,21 @@ impl Mp4Reader { // Update tracks if any fragmented (moof) boxes are found. if !moofs.is_empty() { - let mut default_sample_duration = 0; - if let Some(ref moov) = moov { - if let Some(ref mvex) = &moov.mvex { - default_sample_duration = mvex.trex.default_sample_duration - } - } - for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { for traf in moof.trafs.iter() { let track_id = traf.tfhd.track_id; if let Some(track) = tracks.get_mut(&track_id) { - track.default_sample_duration = default_sample_duration; + track.default_sample_duration = moov + .as_ref() + .and_then(|moov| { + moov.mvex.as_ref().and_then(|mvex| { + mvex.trex + .iter() + .find(|trex| trex.track_id == track.track_id()) + .map(|trex| trex.default_sample_duration) + }) + }) + .unwrap_or(0); track.moof_offsets.push(moof_offset); track.trafs.push(traf.clone()) } else { @@ -186,16 +189,21 @@ impl Mp4Reader { .map(|trak| (trak.tkhd.track_id, Mp4Track::from(trak))) .collect(); - let mut default_sample_duration = 0; - if let Some(ref mvex) = &self.moov.mvex { - default_sample_duration = mvex.trex.default_sample_duration - } - for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { for traf in moof.trafs.iter() { let track_id = traf.tfhd.track_id; if let Some(track) = tracks.get_mut(&track_id) { - track.default_sample_duration = default_sample_duration; + track.default_sample_duration = self + .moov + .mvex + .as_ref() + .and_then(|mvex| { + mvex.trex + .iter() + .find(|trex| trex.track_id == track.track_id()) + .map(|trex| trex.default_sample_duration) + }) + .unwrap_or(0); track.moof_offsets.push(moof_offset); track.trafs.push(traf.clone()) } else {