diff --git a/src/mp4box/data.rs b/src/mp4box/data.rs index 994dd9a..2f54dd0 100644 --- a/src/mp4box/data.rs +++ b/src/mp4box/data.rs @@ -13,6 +13,39 @@ pub struct DataBox { pub data_type: DataType, } +impl DataBox { + pub fn get_type(&self) -> BoxType { + BoxType::DataBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + size += 4; // data_type + size += 4; // reserved + size += self.data.len() as u64; + size + } +} + +impl Mp4Box for DataBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("type={:?} len={}", self.data_type, self.data.len()); + Ok(s) + } +} + impl ReadBox<&mut R> for DataBox { fn read_box(reader: &mut R, size: u64) -> Result { let start = box_start(reader)?; @@ -28,3 +61,58 @@ impl ReadBox<&mut R> for DataBox { Ok(DataBox { data, data_type }) } } + +impl WriteBox<&mut W> for DataBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(self.data_type.clone() as u32)?; + writer.write_u32::(0)?; // reserved = 0 + writer.write_all(&self.data)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_data() { + let src_box = DataBox { + data_type: DataType::Text, + data: b"test_data".to_vec(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::DataBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = DataBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_data_empty() { + let src_box = DataBox::default(); + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::DataBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = DataBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/src/mp4box/ilst.rs b/src/mp4box/ilst.rs index 9609c33..ccb306b 100644 --- a/src/mp4box/ilst.rs +++ b/src/mp4box/ilst.rs @@ -13,6 +13,39 @@ pub struct IlstBox { pub items: HashMap, } +impl IlstBox { + pub fn get_type(&self) -> BoxType { + BoxType::IlstBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + for item in self.items.values() { + size += item.get_size(); + } + size + } +} + +impl Mp4Box for IlstBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("item_count={}", self.items.len()); + Ok(s) + } +} + impl ReadBox<&mut R> for IlstBox { fn read_box(reader: &mut R, size: u64) -> Result { let start = box_start(reader)?; @@ -54,11 +87,36 @@ impl ReadBox<&mut R> for IlstBox { } } +impl WriteBox<&mut W> for IlstBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + for (key, value) in &self.items { + let name = match key { + MetadataKey::Title => BoxType::NameBox, + MetadataKey::Year => BoxType::DayBox, + MetadataKey::Poster => BoxType::CovrBox, + MetadataKey::Summary => BoxType::DescBox, + }; + BoxHeader::new(name, value.get_size()).write(writer)?; + value.data.write_box(writer)?; + } + Ok(size) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] pub struct IlstItemBox { pub data: DataBox, } +impl IlstItemBox { + fn get_size(&self) -> u64 { + HEADER_SIZE + self.data.box_size() + } +} + impl ReadBox<&mut R> for IlstItemBox { fn read_box(reader: &mut R, size: u64) -> Result { let start = box_start(reader)?; @@ -130,3 +188,56 @@ fn item_to_u32(item: &IlstItemBox) -> Option { _ => None, } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_ilst() { + let src_year = IlstItemBox { + data: DataBox { + data_type: DataType::Text, + data: b"test_year".to_vec(), + }, + }; + let src_box = IlstBox { + items: [ + (MetadataKey::Title, IlstItemBox::default()), + (MetadataKey::Year, src_year), + (MetadataKey::Poster, IlstItemBox::default()), + (MetadataKey::Summary, IlstItemBox::default()), + ] + .into(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::IlstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = IlstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_ilst_empty() { + let src_box = IlstBox::default(); + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::IlstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = IlstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/src/mp4box/meta.rs b/src/mp4box/meta.rs index 49347a6..27fe682 100644 --- a/src/mp4box/meta.rs +++ b/src/mp4box/meta.rs @@ -2,13 +2,82 @@ use std::io::{Read, Seek}; use serde::Serialize; +use crate::mp4box::hdlr::HdlrBox; use crate::mp4box::ilst::IlstBox; use crate::mp4box::*; -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] -pub struct MetaBox { - #[serde(skip_serializing_if = "Option::is_none")] - pub ilst: Option, +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "hdlr")] +#[serde(rename_all = "lowercase")] +pub enum MetaBox { + Mdir { + #[serde(skip_serializing_if = "Option::is_none")] + ilst: Option, + }, + + #[serde(skip)] + Unknown { + #[serde(skip)] + hdlr: HdlrBox, + + #[serde(skip)] + data: Vec, + }, +} + +const MDIR: FourCC = FourCC { value: *b"mdir" }; + +impl MetaBox { + pub fn get_type(&self) -> BoxType { + BoxType::MetaBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + match self { + Self::Mdir { ilst } => { + size += HdlrBox::default().box_size(); + if let Some(ilst) = ilst { + size += ilst.box_size(); + } + } + Self::Unknown { hdlr, data } => size += hdlr.box_size() + data.len() as u64, + } + size + } +} + +impl Mp4Box for MetaBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = match self { + Self::Mdir { .. } => "hdlr=ilst".to_string(), + Self::Unknown { hdlr, data } => { + format!("hdlr={} data_len={}", hdlr.handler_type, data.len()) + } + }; + Ok(s) + } +} + +impl Default for MetaBox { + fn default() -> Self { + Self::Unknown { + hdlr: Default::default(), + data: Default::default(), + } + } } impl ReadBox<&mut R> for MetaBox { @@ -20,30 +89,141 @@ impl ReadBox<&mut R> for MetaBox { return Err(Error::UnsupportedBoxVersion(BoxType::UdtaBox, version)); } + let hdlr_header = BoxHeader::read(reader)?; + if hdlr_header.name != BoxType::HdlrBox { + return Err(Error::BoxNotFound(BoxType::HdlrBox)); + } + let hdlr = HdlrBox::read_box(reader, hdlr_header.size)?; + let mut ilst = None; let mut current = reader.seek(SeekFrom::Current(0))?; let end = start + size; - while current < end { - // Get box header. - let header = BoxHeader::read(reader)?; - let BoxHeader { name, size: s } = header; - match name { - BoxType::IlstBox => { - ilst = Some(IlstBox::read_box(reader, s)?); - } - _ => { - // XXX warn!() - skip_box(reader, s)?; + match hdlr.handler_type { + MDIR => { + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::IlstBox => { + ilst = Some(IlstBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.seek(SeekFrom::Current(0))?; } + + Ok(MetaBox::Mdir { ilst }) } + _ => { + let mut data = vec![0u8; (end - current) as usize]; + reader.read_exact(&mut data)?; - current = reader.seek(SeekFrom::Current(0))?; + Ok(MetaBox::Unknown { hdlr, data }) + } } - - skip_bytes_to(reader, start + size)?; - - Ok(MetaBox { ilst }) + } +} + +impl WriteBox<&mut W> for MetaBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, 0, 0)?; + + let hdlr = match self { + Self::Mdir { .. } => HdlrBox { + handler_type: MDIR, + ..Default::default() + }, + Self::Unknown { hdlr, .. } => hdlr.clone(), + }; + hdlr.write_box(writer)?; + + match self { + Self::Mdir { ilst } => { + if let Some(ilst) = ilst { + ilst.write_box(writer)?; + } + } + Self::Unknown { data, .. } => writer.write_all(data)?, + } + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_meta_mdir_empty() { + let src_box = MetaBox::Mdir { ilst: None }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_meta_mdir() { + let src_box = MetaBox::Mdir { + ilst: Some(IlstBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_meta_unknown() { + let src_hdlr = HdlrBox { + handler_type: FourCC::from(*b"test"), + ..Default::default() + }; + let src_data = b"123"; + let src_box = MetaBox::Unknown { + hdlr: src_hdlr, + data: src_data.to_vec(), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); } } diff --git a/src/mp4box/moov.rs b/src/mp4box/moov.rs index 22b6276..b6fd589 100644 --- a/src/mp4box/moov.rs +++ b/src/mp4box/moov.rs @@ -1,6 +1,7 @@ use serde::Serialize; use std::io::{Read, Seek, SeekFrom, Write}; +use crate::meta::MetaBox; use crate::mp4box::*; use crate::mp4box::{mvex::MvexBox, mvhd::MvhdBox, trak::TrakBox, udta::UdtaBox}; @@ -8,6 +9,9 @@ use crate::mp4box::{mvex::MvexBox, mvhd::MvhdBox, trak::TrakBox, udta::UdtaBox}; pub struct MoovBox { pub mvhd: MvhdBox, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub mvex: Option, @@ -28,6 +32,12 @@ impl MoovBox { 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 } } @@ -56,6 +66,7 @@ impl ReadBox<&mut R> for MoovBox { let start = box_start(reader)?; let mut mvhd = None; + let mut meta = None; let mut udta = None; let mut mvex = None; let mut traks = Vec::new(); @@ -71,6 +82,9 @@ impl ReadBox<&mut R> for MoovBox { BoxType::MvhdBox => { mvhd = Some(MvhdBox::read_box(reader, s)?); } + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } BoxType::MvexBox => { mvex = Some(MvexBox::read_box(reader, s)?); } @@ -98,6 +112,7 @@ impl ReadBox<&mut R> for MoovBox { Ok(MoovBox { mvhd: mvhd.unwrap(), + meta, udta, mvex, traks, @@ -114,6 +129,59 @@ impl WriteBox<&mut W> for MoovBox { for trak in self.traks.iter() { trak.write_box(writer)?; } + if let Some(meta) = &self.meta { + meta.write_box(writer)?; + } + if let Some(udta) = &self.udta { + udta.write_box(writer)?; + } Ok(0) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_moov() { + let src_box = MoovBox { + mvhd: MvhdBox::default(), + mvex: None, // XXX mvex is not written currently + traks: vec![], + meta: Some(MetaBox::default()), + udta: Some(UdtaBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MoovBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MoovBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_moov_empty() { + let src_box = MoovBox::default(); + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MoovBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MoovBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } +} diff --git a/src/mp4box/trak.rs b/src/mp4box/trak.rs index c1e1845..cb81e0c 100644 --- a/src/mp4box/trak.rs +++ b/src/mp4box/trak.rs @@ -1,6 +1,7 @@ use serde::Serialize; use std::io::{Read, Seek, SeekFrom, Write}; +use crate::meta::MetaBox; use crate::mp4box::*; use crate::mp4box::{edts::EdtsBox, mdia::MdiaBox, tkhd::TkhdBox}; @@ -11,6 +12,9 @@ pub struct TrakBox { #[serde(skip_serializing_if = "Option::is_none")] pub edts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + pub mdia: MdiaBox, } @@ -55,6 +59,7 @@ impl ReadBox<&mut R> for TrakBox { let mut tkhd = None; let mut edts = None; + let mut meta = None; let mut mdia = None; let mut current = reader.seek(SeekFrom::Current(0))?; @@ -71,6 +76,9 @@ impl ReadBox<&mut R> for TrakBox { BoxType::EdtsBox => { edts = Some(EdtsBox::read_box(reader, s)?); } + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } BoxType::MdiaBox => { mdia = Some(MdiaBox::read_box(reader, s)?); } @@ -95,6 +103,7 @@ impl ReadBox<&mut R> for TrakBox { Ok(TrakBox { tkhd: tkhd.unwrap(), edts, + meta, mdia: mdia.unwrap(), }) } diff --git a/src/mp4box/udta.rs b/src/mp4box/udta.rs index de5dd9b..744d6a4 100644 --- a/src/mp4box/udta.rs +++ b/src/mp4box/udta.rs @@ -11,6 +11,38 @@ pub struct UdtaBox { pub meta: Option, } +impl UdtaBox { + pub fn get_type(&self) -> BoxType { + BoxType::UdtaBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + if let Some(meta) = &self.meta { + size += meta.box_size(); + } + size + } +} + +impl Mp4Box for UdtaBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(String::new()) + } +} + impl ReadBox<&mut R> for UdtaBox { fn read_box(reader: &mut R, size: u64) -> Result { let start = box_start(reader)?; @@ -42,3 +74,58 @@ impl ReadBox<&mut R> for UdtaBox { Ok(UdtaBox { meta }) } } + +impl WriteBox<&mut W> for UdtaBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + if let Some(meta) = &self.meta { + meta.write_box(writer)?; + } + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_udta_empty() { + let src_box = UdtaBox { meta: None }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::UdtaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = UdtaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_udta() { + let src_box = UdtaBox { + meta: Some(MetaBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::UdtaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = UdtaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } +} diff --git a/src/reader.rs b/src/reader.rs index ebb3721..4986442 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use crate::meta::MetaBox; use crate::*; #[derive(Debug)] @@ -170,9 +171,11 @@ impl Mp4Reader { impl Mp4Reader { pub fn metadata(&self) -> impl Metadata<'_> { - self.moov - .udta - .as_ref() - .and_then(|udta| udta.meta.as_ref().and_then(|meta| meta.ilst.as_ref())) + self.moov.udta.as_ref().and_then(|udta| { + udta.meta.as_ref().and_then(|meta| match meta { + MetaBox::Mdir { ilst } => ilst.as_ref(), + _ => None, + }) + }) } }