diff --git a/src/mp4box/emsg.rs b/src/mp4box/emsg.rs new file mode 100644 index 0000000..47482dc --- /dev/null +++ b/src/mp4box/emsg.rs @@ -0,0 +1,237 @@ +use std::ffi::CStr; +use std::io::{Read, Seek, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Default, Serialize)] +pub struct EmsgBox { + pub version: u8, + pub flags: u32, + pub timescale: u32, + pub presentation_time: Option, + pub presentation_time_delta: Option, + pub event_duration: u32, + pub id: u32, + pub scheme_id_uri: String, + pub value: String, + pub message_data: Vec, +} + +impl EmsgBox { + fn size_without_message(version: u8, scheme_id_uri: &str, value: &str) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + + 4 + // id + Self::time_size(version) + + (scheme_id_uri.len() + 1) as u64 + + (value.len() as u64 + 1) as u64 + } + + fn time_size(version: u8) -> u64 { + match version { + 0 => 12, + 1 => 16, + _ => panic!("version must be 0 or 1") + } + } +} + +impl Mp4Box for EmsgBox { + fn box_type(&self) -> BoxType { + BoxType::EmsgBox + } + + fn box_size(&self) -> u64 { + Self::size_without_message(self.version, &self.scheme_id_uri, &self.value) + + self.message_data.len() as u64 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("id={} value={}", self.id, self.value); + Ok(s) + } +} + +impl ReadBox<&mut R> for EmsgBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + let (version, flags) = read_box_header_ext(reader)?; + + let ( + timescale, presentation_time, presentation_time_delta, event_duration, + id, + scheme_id_uri, + value + ) = match version { + 0 => { + let scheme_id_uri = read_null_terminated_utf8_string(reader)?; + let value = read_null_terminated_utf8_string(reader)?; + ( + reader.read_u32::()?, + None, + Some(reader.read_u32::()?), + reader.read_u32::()?, + reader.read_u32::()?, + scheme_id_uri, + value + ) + } + 1 => ( + reader.read_u32::()?, + Some(reader.read_u64::()?), + None, + reader.read_u32::()?, + reader.read_u32::()?, + read_null_terminated_utf8_string(reader)?, + read_null_terminated_utf8_string(reader)? + ), + _ => return Err(Error::InvalidData("version must be 0 or 1")) + }; + + let message_size = size - Self::size_without_message(version, &scheme_id_uri, &value); + let mut message_data = Vec::with_capacity(message_size as usize); + for _ in 0..message_size { + message_data.push(reader.read_u8()?); + } + + skip_bytes_to(reader, start + size)?; + + Ok(EmsgBox { + version, + flags, + timescale, + presentation_time, + presentation_time_delta, + event_duration, + id, + scheme_id_uri, + value, + message_data, + }) + } +} + +impl WriteBox<&mut W> for EmsgBox { + 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, self.version, self.flags)?; + match self.version { + 0 => { + write_null_terminated_str(writer, &self.scheme_id_uri)?; + write_null_terminated_str(writer, &self.value)?; + writer.write_u32::(self.timescale)?; + writer.write_u32::(self.presentation_time_delta.unwrap())?; + writer.write_u32::(self.event_duration)?; + writer.write_u32::(self.id)?; + } + 1 => { + writer.write_u32::(self.timescale)?; + writer.write_u64::(self.presentation_time.unwrap())?; + writer.write_u32::(self.event_duration)?; + writer.write_u32::(self.id)?; + write_null_terminated_str(writer, &self.scheme_id_uri)?; + write_null_terminated_str(writer, &self.value)?; + } + _ => return Err(Error::InvalidData("version must be 0 or 1")) + } + + for &byte in &self.message_data { + writer.write_u8(byte)?; + } + + Ok(size) + } +} + +fn read_null_terminated_utf8_string(reader: &mut R) -> Result { + let mut bytes = Vec::new(); + loop { + let byte = reader.read_u8()?; + bytes.push(byte); + if byte == 0 { break; } + } + if let Ok(str) = unsafe { CStr::from_bytes_with_nul_unchecked(&bytes) }.to_str() { + Ok(str.to_string()) + } else { + Err(Error::InvalidData("invalid utf8")) + } +} + +fn write_null_terminated_str(writer: &mut W, string: &str) -> Result<()> { + for byte in string.bytes() { + writer.write_u8(byte)?; + } + writer.write_u8(0)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::mp4box::BoxHeader; + + use super::*; + + #[test] + fn test_emsg_version0() { + let src_box = EmsgBox { + version: 0, + flags: 0, + timescale: 48000, + presentation_time: None, + presentation_time_delta: Some(100), + event_duration: 200, + id: 8, + scheme_id_uri: String::from("foo"), + value: String::from("foo"), + message_data: vec![1, 2, 3], + }; + 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::EmsgBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = EmsgBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_emsg_version1() { + let src_box = EmsgBox { + version: 1, + flags: 0, + timescale: 48000, + presentation_time: Some(50000), + presentation_time_delta: None, + event_duration: 200, + id: 8, + scheme_id_uri: String::from("foo"), + value: String::from("foo"), + message_data: vec![3, 2, 1], + }; + 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::EmsgBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = EmsgBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} \ No newline at end of file diff --git a/src/mp4box/mod.rs b/src/mp4box/mod.rs index 1cca042..c5f01b1 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -3,6 +3,7 @@ //! * [ISO/IEC 14496-12](https://en.wikipedia.org/wiki/MPEG-4_Part_14) - ISO Base Media File Format (QuickTime, MPEG-4, etc) //! * [ISO/IEC 14496-14](https://en.wikipedia.org/wiki/MPEG-4_Part_14) - MP4 file format //! * ISO/IEC 14496-17 - Streaming text format +//! * [ISO 23009-1](https://www.iso.org/standard/79329.html) -Dynamic adaptive streaming over HTTP (DASH) //! //! http://developer.apple.com/documentation/QuickTime/QTFF/index.html //! http://www.adobe.com/devnet/video/articles/mp4_movie_atom.html @@ -40,6 +41,7 @@ //! mvex //! mehd //! trex +//! emsg //! moof //! mfhd //! traf @@ -71,6 +73,7 @@ pub(crate) mod moov; pub(crate) mod mvex; pub(crate) mod mehd; pub(crate) mod trex; +pub(crate) mod emsg; pub(crate) mod moof; pub(crate) mod mp4a; pub(crate) mod mvhd; @@ -96,6 +99,7 @@ pub(crate) mod vpcc; pub use ftyp::FtypBox; pub use moov::MoovBox; pub use moof::MoofBox; +pub use emsg::EmsgBox; pub const HEADER_SIZE: u64 = 8; // const HEADER_LARGE_SIZE: u64 = 16; @@ -139,6 +143,7 @@ boxtype! { MvexBox => 0x6d766578, MehdBox => 0x6d656864, TrexBox => 0x74726578, + EmsgBox => 0x656d7367, MoofBox => 0x6d6f6f66, TkhdBox => 0x746b6864, TfhdBox => 0x74666864, diff --git a/src/reader.rs b/src/reader.rs index 72aae72..1b5af26 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -11,6 +11,7 @@ pub struct Mp4Reader { pub ftyp: FtypBox, pub moov: MoovBox, pub moofs: Vec, + pub emsgs: Vec, tracks: HashMap, size: u64, @@ -23,6 +24,7 @@ impl Mp4Reader { let mut ftyp = None; let mut moov = None; let mut moofs = Vec::new(); + let mut emsgs = Vec::new(); let mut current = start; while current < size { @@ -48,6 +50,10 @@ impl Mp4Reader { let moof = MoofBox::read_box(&mut reader, s)?; moofs.push(moof); } + BoxType::EmsgBox => { + let emsg = EmsgBox::read_box(&mut reader, s)?; + emsgs.push(emsg); + } _ => { // XXX warn!() skip_box(&mut reader, s)?; @@ -102,6 +108,7 @@ impl Mp4Reader { ftyp: ftyp.unwrap(), moov: moov.unwrap(), moofs, + emsgs, size, tracks, })