mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-06-27 02:29:35 +00:00
1246 lines
38 KiB
Rust
1246 lines
38 KiB
Rust
//! Module for parsing ISO Base Media Format aka video/mp4 streams.
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
mod byteorder; // 'extern crate' upstream.
|
|
use byteorder::ReadBytesExt;
|
|
use std::error::Error as ErrorTrait; // For Err(e) => e.description().
|
|
use std::io::{Read, BufRead, Take};
|
|
use std::cmp;
|
|
use std::fmt;
|
|
|
|
// Expose C api wrapper.
|
|
pub mod capi;
|
|
// FIXME: We can 'pub use capi::*' in rustc 1.5 and later.
|
|
pub use capi::{mp4parse_new, mp4parse_free, mp4parse_read};
|
|
|
|
/// Describes parser failures.
|
|
///
|
|
/// This enum wraps athe standard `io::Error` type, unified with
|
|
/// our own parser error states and those of crates we use.
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
/// Parse error caused by corrupt or malformed data.
|
|
InvalidData,
|
|
/// Parse error caused by limited parser support rather than invalid data.
|
|
Unsupported,
|
|
/// Reflect `byteorder::Error::UnexpectedEOF` for short data.
|
|
UnexpectedEOF,
|
|
/// Propagate underlying errors from `std::io`.
|
|
Io(std::io::Error),
|
|
}
|
|
|
|
impl From<std::io::Error> for Error {
|
|
fn from(err: std::io::Error) -> Error { Error::Io(err) }
|
|
}
|
|
|
|
impl From<byteorder::Error> for Error {
|
|
fn from(err: byteorder::Error) -> Error {
|
|
match err {
|
|
byteorder::Error::UnexpectedEOF => Error::UnexpectedEOF,
|
|
byteorder::Error::Io(e) => Error::Io(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result shorthand using our Error enum.
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
/// Four-byte 'character code' describing the type of a piece of data.
|
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
|
pub struct FourCC([u8; 4]);
|
|
|
|
impl fmt::Debug for FourCC {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "'{}'", String::from_utf8_lossy(&self.0))
|
|
}
|
|
}
|
|
|
|
/// Basic ISO box structure.
|
|
///
|
|
/// mp4 files are a sequence of possibly-nested 'box' structures.
|
|
/// Each box begins with a header describing the length of the
|
|
/// box's data and a four-byte 'character code' or `FourCC` which
|
|
/// identifies the type of the box. Together these are enough to
|
|
/// interpret the contents of that section of the file.
|
|
#[derive(Debug)]
|
|
pub struct BoxHeader {
|
|
/// Four character box type.
|
|
pub name: FourCC,
|
|
/// Size of the box in bytes.
|
|
pub size: u64,
|
|
/// Offset to the start of the contained data (or header size).
|
|
pub offset: u64,
|
|
}
|
|
|
|
/// File type box 'ftyp'.
|
|
#[derive(Debug)]
|
|
struct FileTypeBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
major_brand: FourCC,
|
|
minor_version: u32,
|
|
compatible_brands: Vec<FourCC>,
|
|
}
|
|
|
|
/// Movie header box 'mvhd'.
|
|
#[derive(Debug)]
|
|
struct MovieHeaderBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
timescale: u32,
|
|
duration: u64,
|
|
// Ignore other fields.
|
|
}
|
|
|
|
/// Track header box 'tkhd'
|
|
#[derive(Debug, Clone)]
|
|
struct TrackHeaderBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
track_id: u32,
|
|
enabled: bool,
|
|
duration: u64,
|
|
width: u32,
|
|
height: u32,
|
|
}
|
|
|
|
/// Edit list box 'elst'
|
|
#[derive(Debug)]
|
|
struct EditListBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
edits: Vec<Edit>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Edit {
|
|
segment_duration: u64,
|
|
media_time: i64,
|
|
media_rate_integer: i16,
|
|
media_rate_fraction: i16,
|
|
}
|
|
|
|
/// Media header box 'mdhd'
|
|
#[derive(Debug)]
|
|
struct MediaHeaderBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
timescale: u32,
|
|
duration: u64,
|
|
}
|
|
|
|
// Chunk offset box 'stco' or 'co64'
|
|
#[derive(Debug)]
|
|
struct ChunkOffsetBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
offsets: Vec<u64>,
|
|
}
|
|
|
|
// Sync sample box 'stss'
|
|
#[derive(Debug)]
|
|
struct SyncSampleBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
samples: Vec<u32>,
|
|
}
|
|
|
|
// Sample to chunk box 'stsc'
|
|
#[derive(Debug)]
|
|
struct SampleToChunkBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
samples: Vec<SampleToChunk>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SampleToChunk {
|
|
first_chunk: u32,
|
|
samples_per_chunk: u32,
|
|
sample_description_index: u32,
|
|
}
|
|
|
|
// Sample size box 'stsz'
|
|
#[derive(Debug)]
|
|
struct SampleSizeBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
sample_size: u32,
|
|
sample_sizes: Vec<u32>,
|
|
}
|
|
|
|
// Time to sample box 'stts'
|
|
#[derive(Debug)]
|
|
struct TimeToSampleBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
samples: Vec<Sample>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Sample {
|
|
sample_count: u32,
|
|
sample_delta: u32,
|
|
}
|
|
|
|
// Handler reference box 'hdlr'
|
|
#[derive(Debug)]
|
|
struct HandlerBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
handler_type: FourCC,
|
|
}
|
|
|
|
// Sample description box 'stsd'
|
|
#[derive(Debug)]
|
|
struct SampleDescriptionBox {
|
|
name: FourCC,
|
|
size: u64,
|
|
descriptions: Vec<SampleEntry>,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone)]
|
|
enum SampleEntry {
|
|
Audio(AudioSampleEntry),
|
|
Video(VideoSampleEntry),
|
|
Unknown,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct AudioSampleEntry {
|
|
data_reference_index: u16,
|
|
channelcount: u16,
|
|
samplesize: u16,
|
|
samplerate: u32,
|
|
esds: ES_Descriptor,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct VideoSampleEntry {
|
|
data_reference_index: u16,
|
|
width: u16,
|
|
height: u16,
|
|
avcc: AVCDecoderConfigurationRecord,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone)]
|
|
struct AVCDecoderConfigurationRecord {
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
#[allow(non_camel_case_types)]
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone)]
|
|
struct ES_Descriptor {
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
/// Internal data structures.
|
|
#[derive(Debug)]
|
|
pub struct MediaContext {
|
|
timescale: Option<MediaTimeScale>,
|
|
/// Tracks found in the file.
|
|
tracks: Vec<Track>,
|
|
/// Print boxes and other info as parsing proceeds. For debugging.
|
|
trace: bool,
|
|
}
|
|
|
|
impl MediaContext {
|
|
pub fn new() -> MediaContext {
|
|
MediaContext {
|
|
timescale: None,
|
|
tracks: Vec::new(),
|
|
trace: false,
|
|
}
|
|
}
|
|
|
|
pub fn trace(&mut self, on: bool) {
|
|
self.trace = on;
|
|
}
|
|
}
|
|
|
|
macro_rules! log {
|
|
( $ctx:expr, $( $args:expr),* ) => {
|
|
if $ctx.trace {
|
|
println!( $( $args, )* );
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum TrackType {
|
|
Audio,
|
|
Video,
|
|
Unknown,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
struct MediaTimeScale(u64); /// The media's global (mvhd) timescale.
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
struct MediaScaledTime(u64); /// A time scaled by the media's global (mvhd) timescale.
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
struct TrackTimeScale(u64, usize); /// The track's local (mdhd) timescale.
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
struct TrackScaledTime(u64, usize); /// A time scaled by the track's local (mdhd) timescale.
|
|
|
|
#[derive(Debug)]
|
|
struct Track {
|
|
track_type: TrackType,
|
|
empty_duration: Option<MediaScaledTime>,
|
|
media_time: Option<TrackScaledTime>,
|
|
timescale: Option<TrackTimeScale>,
|
|
duration: Option<TrackScaledTime>,
|
|
track_id: Option<u32>,
|
|
mime_type: String,
|
|
data: Option<SampleEntry>,
|
|
tkhd: Option<TrackHeaderBox>, // TODO(kinetik): find a nicer way to export this.
|
|
}
|
|
|
|
impl Track {
|
|
fn new() -> Track {
|
|
Track {
|
|
track_type: TrackType::Unknown,
|
|
empty_duration: None,
|
|
media_time: None,
|
|
timescale: None,
|
|
duration: None,
|
|
track_id: None,
|
|
mime_type: String::new(),
|
|
data: None,
|
|
tkhd: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read and parse a box header.
|
|
///
|
|
/// Call this first to determine the type of a particular mp4 box
|
|
/// and its length. Used internally for dispatching to specific
|
|
/// parsers for the internal content, or to get the length to
|
|
/// skip unknown or uninteresting boxes.
|
|
pub fn read_box_header<T: ReadBytesExt>(src: &mut T) -> Result<BoxHeader> {
|
|
let size32 = try!(be_u32(src));
|
|
let name = try!(be_fourcc(src));
|
|
let size = match size32 {
|
|
0 => return Err(Error::Unsupported),
|
|
1 => {
|
|
let size64 = try!(be_u64(src));
|
|
if size64 < 16 {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
size64
|
|
},
|
|
2 ... 7 => return Err(Error::InvalidData),
|
|
_ => size32 as u64,
|
|
};
|
|
let offset = match size32 {
|
|
1 => 4 + 4 + 8,
|
|
_ => 4 + 4,
|
|
};
|
|
assert!(offset <= size);
|
|
Ok(BoxHeader{
|
|
name: name,
|
|
size: size,
|
|
offset: offset,
|
|
})
|
|
}
|
|
|
|
/// Parse the extra header fields for a full box.
|
|
fn read_fullbox_extra<T: ReadBytesExt>(src: &mut T) -> Result<(u8, u32)> {
|
|
let version = try!(src.read_u8());
|
|
let flags_a = try!(src.read_u8());
|
|
let flags_b = try!(src.read_u8());
|
|
let flags_c = try!(src.read_u8());
|
|
Ok((version, (flags_a as u32) << 16 |
|
|
(flags_b as u32) << 8 |
|
|
(flags_c as u32)))
|
|
}
|
|
|
|
/// Skip over the entire contents of a box.
|
|
fn skip_box_content<T: BufRead> (src: &mut T, header: &BoxHeader) -> Result<usize> {
|
|
skip(src, (header.size - header.offset) as usize)
|
|
}
|
|
|
|
/// Skip over the remaining contents of a box.
|
|
fn skip_remaining_box_content<T: BufRead> (src: &mut T, header: &BoxHeader) -> Result<()> {
|
|
match skip(src, (header.size - header.offset) as usize) {
|
|
Ok(_) | Err(Error::UnexpectedEOF) => Ok(()),
|
|
e @ _ => Err(e.err().unwrap())
|
|
}
|
|
}
|
|
|
|
/// Helper to construct a Take over the contents of a box.
|
|
fn limit<'a, T: BufRead>(f: &'a mut T, h: &BoxHeader) -> Take<&'a mut T> {
|
|
f.take(h.size - h.offset)
|
|
}
|
|
|
|
fn driver<F, T: BufRead>(f: &mut T, context: &mut MediaContext, action: F) -> Result<()>
|
|
where F: Fn(&mut MediaContext, BoxHeader, &mut Take<&mut T>) -> Result<()> {
|
|
loop {
|
|
let r = read_box_header(f).and_then(|h| {
|
|
let mut content = limit(f, &h);
|
|
let r = action(context, h, &mut content);
|
|
if let Ok(_) = r {
|
|
// TODO(kinetik): can check this for "non-fatal" errors (e.g. EOF) too.
|
|
log!(context, "{} content bytes left", content.limit());
|
|
assert!(content.limit() == 0);
|
|
log!(context, "read_box context: {:?}", context);
|
|
}
|
|
r
|
|
});
|
|
match r {
|
|
Ok(_) => { },
|
|
Err(Error::UnexpectedEOF) => {
|
|
// byteorder returns EOF at the end of the buffer.
|
|
// This isn't an error for us, just an signal to
|
|
// stop recursion.
|
|
log!(context, "Caught Error::UnexpectedEOF");
|
|
break;
|
|
},
|
|
Err(Error::InvalidData) => {
|
|
log!(context, "Invalid data");
|
|
return Err(Error::InvalidData);
|
|
},
|
|
Err(Error::Unsupported) => {
|
|
log!(context, "Unsupported BMFF construct");
|
|
return Err(Error::Unsupported);
|
|
},
|
|
Err(Error::Io(e)) => {
|
|
log!(context, "I/O Error '{:?}' reading box: {:?}",
|
|
e.kind(), e.description());
|
|
return Err(Error::Io(e));
|
|
},
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Read the contents of a box, including sub boxes.
|
|
///
|
|
/// Metadata is accumulated in the passed-through MediaContext struct,
|
|
/// which can be examined later.
|
|
pub fn read_mp4<T: BufRead>(f: &mut T, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"ftyp" => {
|
|
let ftyp = try!(read_ftyp(&mut content, &h));
|
|
log!(context, "{:?}", ftyp);
|
|
},
|
|
b"moov" => try!(read_moov(&mut content, &h, context)),
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn read_moov<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"mvhd" => {
|
|
let mvhd = try!(read_mvhd(&mut content, &h));
|
|
context.timescale = Some(MediaTimeScale(mvhd.timescale as u64));
|
|
log!(context, " {:?}", mvhd);
|
|
},
|
|
b"trak" => {
|
|
context.tracks.push(Track::new());
|
|
try!(read_trak(&mut content, &h, context));
|
|
},
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn read_trak<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"tkhd" => {
|
|
let tkhd = try!(read_tkhd(&mut content, &h));
|
|
if let Some(track) = context.tracks.last_mut() {
|
|
track.track_id = Some(tkhd.track_id);
|
|
track.tkhd = Some(tkhd.clone())
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
log!(context, " {:?}", tkhd);
|
|
},
|
|
b"edts" => try!(read_edts(&mut content, &h, context)),
|
|
b"mdia" => try!(read_mdia(&mut content, &h, context)),
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(()) // and_then needs a Result to return.
|
|
})
|
|
}
|
|
|
|
fn read_edts<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"elst" => {
|
|
let elst = try!(read_elst(&mut content, &h));
|
|
let mut empty_duration = 0;
|
|
let track_idx = context.tracks.len() - 1;
|
|
if let Some(track) = context.tracks.last_mut() {
|
|
let mut idx = 0;
|
|
if elst.edits.len() > 2 {
|
|
return Err(Error::Unsupported);
|
|
}
|
|
if elst.edits[idx].media_time == -1 {
|
|
empty_duration = elst.edits[0].segment_duration;
|
|
idx += 1;
|
|
}
|
|
track.empty_duration = Some(MediaScaledTime(empty_duration));
|
|
if elst.edits[idx].media_time < 0 {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
track.media_time = Some(TrackScaledTime(elst.edits[idx].media_time as u64,
|
|
track_idx));
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
log!(context, " {:?}", elst);
|
|
},
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn read_mdia<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"mdhd" => {
|
|
let mdhd = try!(read_mdhd(&mut content, &h));
|
|
let track_idx = context.tracks.len() - 1;
|
|
if let Some(track) = context.tracks.last_mut() {
|
|
track.duration = Some(TrackScaledTime(mdhd.duration, track_idx));
|
|
track.timescale = Some(TrackTimeScale(mdhd.timescale as u64,
|
|
track_idx));
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
log!(context, " {:?}", mdhd);
|
|
},
|
|
b"hdlr" => {
|
|
let hdlr = try!(read_hdlr(&mut content, &h));
|
|
if let Some(track) = context.tracks.last_mut() {
|
|
match &hdlr.handler_type.0 {
|
|
b"vide" => track.track_type = TrackType::Video,
|
|
b"soun" => track.track_type = TrackType::Audio,
|
|
_ => ()
|
|
}
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
log!(context, " {:?}", hdlr);
|
|
},
|
|
b"minf" => try!(read_minf(&mut content, &h, context)),
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn read_minf<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"stbl" => try!(read_stbl(&mut content, &h, context)),
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn read_stbl<T: BufRead>(f: &mut T, _: &BoxHeader, context: &mut MediaContext) -> Result<()> {
|
|
driver(f, context, |context, h, mut content| {
|
|
match &h.name.0 {
|
|
b"stsd" => {
|
|
if let Some(track) = context.tracks.last_mut() {
|
|
let stsd = try!(read_stsd(&mut content, &h, track));
|
|
log!(context, " {:?}", stsd);
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
},
|
|
b"stts" => {
|
|
let stts = try!(read_stts(&mut content, &h));
|
|
log!(context, " {:?}", stts);
|
|
},
|
|
b"stsc" => {
|
|
let stsc = try!(read_stsc(&mut content, &h));
|
|
log!(context, " {:?}", stsc);
|
|
},
|
|
b"stsz" => {
|
|
let stsz = try!(read_stsz(&mut content, &h));
|
|
log!(context, " {:?}", stsz);
|
|
},
|
|
b"stco" => {
|
|
let stco = try!(read_stco(&mut content, &h));
|
|
log!(context, " {:?}", stco);
|
|
},
|
|
b"co64" => {
|
|
let co64 = try!(read_co64(&mut content, &h));
|
|
log!(context, " {:?}", co64);
|
|
},
|
|
b"stss" => {
|
|
let stss = try!(read_stss(&mut content, &h));
|
|
log!(context, " {:?}", stss);
|
|
},
|
|
_ => {
|
|
// Skip the contents of unknown chunks.
|
|
log!(context, "{:?} (skipped)", h);
|
|
try!(skip_box_content(&mut content, &h));
|
|
},
|
|
};
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Parse an ftyp box.
|
|
fn read_ftyp<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<FileTypeBox> {
|
|
let major = try!(be_fourcc(src));
|
|
let minor = try!(be_u32(src));
|
|
let bytes_left = head.size - head.offset - 8;
|
|
if bytes_left % 4 != 0 {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
// Is a brand_count of zero valid?
|
|
let brand_count = bytes_left / 4;
|
|
let mut brands = Vec::new();
|
|
for _ in 0..brand_count {
|
|
brands.push(try!(be_fourcc(src)));
|
|
}
|
|
Ok(FileTypeBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
major_brand: major,
|
|
minor_version: minor,
|
|
compatible_brands: brands,
|
|
})
|
|
}
|
|
|
|
/// Parse an mvhd box.
|
|
fn read_mvhd<T: ReadBytesExt + BufRead>(src: &mut T, head: &BoxHeader) -> Result<MovieHeaderBox> {
|
|
let (version, _) = try!(read_fullbox_extra(src));
|
|
match version {
|
|
// 64 bit creation and modification times.
|
|
1 => { try!(skip(src, 16)); },
|
|
// 32 bit creation and modification times.
|
|
0 => { try!(skip(src, 8)); },
|
|
_ => return Err(Error::InvalidData),
|
|
}
|
|
let timescale = try!(be_u32(src));
|
|
let duration = match version {
|
|
1 => try!(be_u64(src)),
|
|
0 => try!(be_u32(src)) as u64,
|
|
_ => return Err(Error::InvalidData),
|
|
};
|
|
// Skip remaining fields.
|
|
try!(skip(src, 80));
|
|
Ok(MovieHeaderBox {
|
|
name: head.name,
|
|
size: head.size,
|
|
timescale: timescale,
|
|
duration: duration,
|
|
})
|
|
}
|
|
|
|
/// Parse a tkhd box.
|
|
fn read_tkhd<T: ReadBytesExt + BufRead>(src: &mut T, head: &BoxHeader) -> Result<TrackHeaderBox> {
|
|
let (version, flags) = try!(read_fullbox_extra(src));
|
|
let disabled = flags & 0x1u32 == 0 || flags & 0x2u32 == 0;
|
|
match version {
|
|
// 64 bit creation and modification times.
|
|
1 => { try!(skip(src, 16)); },
|
|
// 32 bit creation and modification times.
|
|
0 => { try!(skip(src, 8)); },
|
|
_ => return Err(Error::InvalidData),
|
|
}
|
|
let track_id = try!(be_u32(src));
|
|
try!(skip(src, 4));
|
|
let duration = match version {
|
|
1 => try!(be_u64(src)),
|
|
0 => try!(be_u32(src)) as u64,
|
|
_ => return Err(Error::InvalidData),
|
|
};
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 52));
|
|
let width = try!(be_u32(src));
|
|
let height = try!(be_u32(src));
|
|
Ok(TrackHeaderBox {
|
|
name: head.name,
|
|
size: head.size,
|
|
track_id: track_id,
|
|
enabled: !disabled,
|
|
duration: duration,
|
|
width: width,
|
|
height: height,
|
|
})
|
|
}
|
|
|
|
/// Parse a elst box.
|
|
fn read_elst<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<EditListBox> {
|
|
let (version, _) = try!(read_fullbox_extra(src));
|
|
let edit_count = try!(be_u32(src));
|
|
let mut edits = Vec::new();
|
|
for _ in 0..edit_count {
|
|
let (segment_duration, media_time) = match version {
|
|
1 => {
|
|
// 64 bit segment duration and media times.
|
|
(try!(be_u64(src)),
|
|
try!(be_i64(src)))
|
|
},
|
|
0 => {
|
|
// 32 bit segment duration and media times.
|
|
(try!(be_u32(src)) as u64,
|
|
try!(be_i32(src)) as i64)
|
|
},
|
|
_ => return Err(Error::InvalidData),
|
|
};
|
|
let media_rate_integer = try!(be_i16(src));
|
|
let media_rate_fraction = try!(be_i16(src));
|
|
edits.push(Edit{
|
|
segment_duration: segment_duration,
|
|
media_time: media_time,
|
|
media_rate_integer: media_rate_integer,
|
|
media_rate_fraction: media_rate_fraction,
|
|
})
|
|
}
|
|
|
|
Ok(EditListBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
edits: edits
|
|
})
|
|
}
|
|
|
|
/// Parse a mdhd box.
|
|
fn read_mdhd<T: ReadBytesExt + BufRead>(src: &mut T, head: &BoxHeader) -> Result<MediaHeaderBox> {
|
|
let (version, _) = try!(read_fullbox_extra(src));
|
|
let (timescale, duration) = match version {
|
|
1 => {
|
|
// Skip 64-bit creation and modification times.
|
|
try!(skip(src, 16));
|
|
|
|
// 64 bit duration.
|
|
(try!(be_u32(src)),
|
|
try!(be_u64(src)))
|
|
},
|
|
0 => {
|
|
// Skip 32-bit creation and modification times.
|
|
try!(skip(src, 8));
|
|
|
|
// 32 bit duration.
|
|
(try!(be_u32(src)),
|
|
try!(be_u32(src)) as u64)
|
|
},
|
|
_ => return Err(Error::InvalidData),
|
|
};
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 4));
|
|
|
|
Ok(MediaHeaderBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
timescale: timescale,
|
|
duration: duration,
|
|
})
|
|
}
|
|
|
|
/// Parse a stco box.
|
|
fn read_stco<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<ChunkOffsetBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let offset_count = try!(be_u32(src));
|
|
let mut offsets = Vec::new();
|
|
for _ in 0..offset_count {
|
|
offsets.push(try!(be_u32(src)) as u64);
|
|
}
|
|
|
|
Ok(ChunkOffsetBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
offsets: offsets,
|
|
})
|
|
}
|
|
|
|
/// Parse a stco box.
|
|
fn read_co64<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<ChunkOffsetBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let offset_count = try!(be_u32(src));
|
|
let mut offsets = Vec::new();
|
|
for _ in 0..offset_count {
|
|
offsets.push(try!(be_u64(src)));
|
|
}
|
|
|
|
Ok(ChunkOffsetBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
offsets: offsets,
|
|
})
|
|
}
|
|
|
|
/// Parse a stss box.
|
|
fn read_stss<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<SyncSampleBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let sample_count = try!(be_u32(src));
|
|
let mut samples = Vec::new();
|
|
for _ in 0..sample_count {
|
|
samples.push(try!(be_u32(src)));
|
|
}
|
|
|
|
Ok(SyncSampleBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
samples: samples,
|
|
})
|
|
}
|
|
|
|
/// Parse a stsc box.
|
|
fn read_stsc<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<SampleToChunkBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let sample_count = try!(be_u32(src));
|
|
let mut samples = Vec::new();
|
|
for _ in 0..sample_count {
|
|
let first_chunk = try!(be_u32(src));
|
|
let samples_per_chunk = try!(be_u32(src));
|
|
let sample_description_index = try!(be_u32(src));
|
|
samples.push(SampleToChunk{
|
|
first_chunk: first_chunk,
|
|
samples_per_chunk: samples_per_chunk,
|
|
sample_description_index: sample_description_index
|
|
});
|
|
}
|
|
|
|
Ok(SampleToChunkBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
samples: samples,
|
|
})
|
|
}
|
|
|
|
/// Parse a stsz box.
|
|
fn read_stsz<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<SampleSizeBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let sample_size = try!(be_u32(src));
|
|
let sample_count = try!(be_u32(src));
|
|
let mut sample_sizes = Vec::new();
|
|
if sample_size == 0 {
|
|
for _ in 0..sample_count {
|
|
sample_sizes.push(try!(be_u32(src)));
|
|
}
|
|
}
|
|
|
|
Ok(SampleSizeBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
sample_size: sample_size,
|
|
sample_sizes: sample_sizes,
|
|
})
|
|
}
|
|
|
|
/// Parse a stts box.
|
|
fn read_stts<T: ReadBytesExt>(src: &mut T, head: &BoxHeader) -> Result<TimeToSampleBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let sample_count = try!(be_u32(src));
|
|
let mut samples = Vec::new();
|
|
for _ in 0..sample_count {
|
|
let sample_count = try!(be_u32(src));
|
|
let sample_delta = try!(be_u32(src));
|
|
samples.push(Sample{
|
|
sample_count: sample_count,
|
|
sample_delta: sample_delta,
|
|
});
|
|
}
|
|
|
|
Ok(TimeToSampleBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
samples: samples,
|
|
})
|
|
}
|
|
|
|
/// Parse a hdlr box.
|
|
fn read_hdlr<T: ReadBytesExt + BufRead>(src: &mut T, head: &BoxHeader) -> Result<HandlerBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 4));
|
|
|
|
let handler_type = try!(be_fourcc(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 12));
|
|
|
|
// TODO(kinetik): Find a copy of ISO/IEC 14496-1 to work out how strings are encoded.
|
|
// As a hack, just consume the rest of the box.
|
|
try!(skip_remaining_box_content(src, head));
|
|
|
|
Ok(HandlerBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
handler_type: handler_type,
|
|
})
|
|
}
|
|
|
|
/// Parse a stsd box.
|
|
fn read_stsd<T: ReadBytesExt + BufRead>(src: &mut T, head: &BoxHeader, track: &mut Track) -> Result<SampleDescriptionBox> {
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
|
|
let description_count = try!(be_u32(src));
|
|
let mut descriptions = Vec::new();
|
|
|
|
// TODO(kinetik): check if/when more than one desc per track? do we need to support?
|
|
for _ in 0..description_count {
|
|
let description = match track.track_type {
|
|
TrackType::Video => {
|
|
let h = try!(read_box_header(src));
|
|
// TODO(kinetik): encv here also?
|
|
if &h.name.0 != b"avc1" && &h.name.0 != b"avc3" {
|
|
return Err(Error::Unsupported);
|
|
}
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 6));
|
|
|
|
let data_reference_index = try!(be_u16(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 16));
|
|
|
|
let width = try!(be_u16(src));
|
|
let height = try!(be_u16(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 50));
|
|
|
|
// TODO(kinetik): Parse avcC atom? For now we just stash the data.
|
|
let h = try!(read_box_header(src));
|
|
if &h.name.0 != b"avcC" {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
let mut data: Vec<u8> = vec![0; (h.size - h.offset) as usize];
|
|
let r = try!(src.read(&mut data));
|
|
assert!(r == data.len());
|
|
let avcc = AVCDecoderConfigurationRecord { data: data };
|
|
|
|
try!(skip_remaining_box_content(src, head));
|
|
|
|
track.mime_type = String::from("video/avc");
|
|
|
|
SampleEntry::Video(VideoSampleEntry {
|
|
data_reference_index: data_reference_index,
|
|
width: width,
|
|
height: height,
|
|
avcc: avcc,
|
|
})
|
|
},
|
|
TrackType::Audio => {
|
|
let h = try!(read_box_header(src));
|
|
// TODO(kinetik): enca here also?
|
|
if &h.name.0 != b"mp4a" {
|
|
return Err(Error::Unsupported);
|
|
}
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 6));
|
|
|
|
let data_reference_index = try!(be_u16(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 8));
|
|
|
|
let channelcount = try!(be_u16(src));
|
|
let samplesize = try!(be_u16(src));
|
|
|
|
// Skip uninteresting fields.
|
|
try!(skip(src, 4));
|
|
|
|
let samplerate = try!(be_u32(src));
|
|
|
|
// TODO(kinetik): Parse esds atom? For now we just stash the data.
|
|
let h = try!(read_box_header(src));
|
|
if &h.name.0 != b"esds" {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
let (_, _) = try!(read_fullbox_extra(src));
|
|
let mut data: Vec<u8> = vec![0; (h.size - h.offset - 4) as usize];
|
|
let r = try!(src.read(&mut data));
|
|
assert!(r == data.len());
|
|
let esds = ES_Descriptor { data: data };
|
|
|
|
// TODO(kinetik): stagefright inspects ESDS to detect MP3 (audio/mpeg).
|
|
track.mime_type = String::from("audio/mp4a-latm");
|
|
|
|
SampleEntry::Audio(AudioSampleEntry {
|
|
data_reference_index: data_reference_index,
|
|
channelcount: channelcount,
|
|
samplesize: samplesize,
|
|
samplerate: samplerate,
|
|
esds: esds,
|
|
})
|
|
},
|
|
TrackType::Unknown => {
|
|
SampleEntry::Unknown
|
|
}
|
|
};
|
|
if track.data.is_none() {
|
|
track.data = Some(description.clone());
|
|
} else {
|
|
return Err(Error::InvalidData);
|
|
}
|
|
descriptions.push(description);
|
|
}
|
|
|
|
Ok(SampleDescriptionBox{
|
|
name: head.name,
|
|
size: head.size,
|
|
descriptions: descriptions,
|
|
})
|
|
}
|
|
|
|
/// Skip a number of bytes that we don't care to parse.
|
|
fn skip<T: BufRead>(src: &mut T, bytes: usize) -> Result<usize> {
|
|
let mut bytes_to_skip = bytes;
|
|
while bytes_to_skip > 0 {
|
|
let len = try!(src.fill_buf()).len();
|
|
if len == 0 {
|
|
return Err(Error::UnexpectedEOF);
|
|
}
|
|
let discard = cmp::min(len, bytes_to_skip);
|
|
src.consume(discard);
|
|
bytes_to_skip -= discard;
|
|
}
|
|
assert!(bytes_to_skip == 0);
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn media_time_to_ms(time: MediaScaledTime, scale: MediaTimeScale) -> u64 {
|
|
assert!(scale.0 != 0);
|
|
time.0 * 1000000 / scale.0
|
|
}
|
|
|
|
fn track_time_to_ms(time: TrackScaledTime, scale: TrackTimeScale) -> u64 {
|
|
assert!(time.1 == scale.1);
|
|
assert!(scale.0 != 0);
|
|
time.0 * 1000000 / scale.0
|
|
}
|
|
|
|
fn be_i16<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<i16> {
|
|
src.read_i16::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_i32<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<i32> {
|
|
src.read_i32::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_i64<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<i64> {
|
|
src.read_i64::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_u16<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<u16> {
|
|
src.read_u16::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_u32<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<u32> {
|
|
src.read_u32::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_u64<T: ReadBytesExt>(src: &mut T) -> byteorder::Result<u64> {
|
|
src.read_u64::<byteorder::BigEndian>()
|
|
}
|
|
|
|
fn be_fourcc<T: Read>(src: &mut T) -> Result<FourCC> {
|
|
let mut fourcc = [0; 4];
|
|
match src.read(&mut fourcc) {
|
|
// Expect all 4 bytes read.
|
|
Ok(4) => Ok(FourCC(fourcc)),
|
|
// Short read means EOF.
|
|
Ok(_) => Err(Error::UnexpectedEOF),
|
|
// Propagate std::io errors.
|
|
Err(e) => Err(Error::Io(e)),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_box_header() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 8]; // minimal box length
|
|
write!(&mut test, "test").unwrap(); // box type
|
|
let mut stream = Cursor::new(test);
|
|
let parsed = read_box_header(&mut stream).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"test"));
|
|
assert_eq!(parsed.size, 8);
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_box_header_long() {
|
|
use std::io::Cursor;
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 1]; // long box extension code
|
|
test.extend("long".to_string().into_bytes()); // box type
|
|
test.extend(vec![0, 0, 0, 0, 0, 0, 16, 0]); // 64 bit size
|
|
// Skip generating box content.
|
|
let mut stream = Cursor::new(test);
|
|
let parsed = read_box_header(&mut stream).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"long"));
|
|
assert_eq!(parsed.size, 4096);
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_ftyp() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 24]; // size
|
|
write!(&mut test, "ftyp").unwrap(); // type
|
|
write!(&mut test, "mp42").unwrap(); // major brand
|
|
test.extend(vec![0, 0, 0, 0]); // minor version
|
|
write!(&mut test, "isom").unwrap(); // compatible brands...
|
|
write!(&mut test, "mp42").unwrap();
|
|
assert_eq!(test.len(), 24);
|
|
|
|
let mut stream = Cursor::new(test);
|
|
let header = read_box_header(&mut stream).unwrap();
|
|
let parsed = read_ftyp(&mut stream, &header).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"ftyp"));
|
|
assert_eq!(parsed.size, 24);
|
|
assert_eq!(parsed.major_brand, FourCC(*b"mp42"));
|
|
assert_eq!(parsed.minor_version, 0);
|
|
assert_eq!(parsed.compatible_brands.len(), 2);
|
|
assert_eq!(parsed.compatible_brands[0], FourCC(*b"isom"));
|
|
assert_eq!(parsed.compatible_brands[1], FourCC(*b"mp42"));
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_elst_v0() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 28]; // size
|
|
write!(&mut test, "elst").unwrap(); // type
|
|
test.extend(vec![0, 0, 0, 0]); // fullbox
|
|
test.extend(vec![0, 0, 0, 1]); // count
|
|
test.extend(vec![1, 2, 3, 4,
|
|
5, 6, 7, 8,
|
|
9, 10,
|
|
11, 12]);
|
|
assert_eq!(test.len(), 28);
|
|
|
|
let mut stream = Cursor::new(test);
|
|
let header = read_box_header(&mut stream).unwrap();
|
|
let parsed = read_elst(&mut stream, &header).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"elst"));
|
|
assert_eq!(parsed.size, 28);
|
|
assert_eq!(parsed.edits.len(), 1);
|
|
assert_eq!(parsed.edits[0].segment_duration, 16909060);
|
|
assert_eq!(parsed.edits[0].media_time, 84281096);
|
|
assert_eq!(parsed.edits[0].media_rate_integer, 2314);
|
|
assert_eq!(parsed.edits[0].media_rate_fraction, 2828);
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_elst_v1() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 56]; // size
|
|
write!(&mut test, "elst").unwrap(); // type
|
|
test.extend(vec![1, 0, 0, 0]); // fullbox
|
|
test.extend(vec![0, 0, 0, 2]); // count
|
|
test.extend(vec![1, 2, 3, 4, 1, 2, 3, 4,
|
|
5, 6, 7, 8, 5, 6, 7, 8,
|
|
9, 10,
|
|
11, 12]);
|
|
test.extend(vec![1, 2, 3, 4, 1, 2, 3, 4,
|
|
5, 6, 7, 8, 5, 6, 7, 8,
|
|
9, 10,
|
|
11, 12]);
|
|
assert_eq!(test.len(), 56);
|
|
|
|
let mut stream = Cursor::new(test);
|
|
let header = read_box_header(&mut stream).unwrap();
|
|
let parsed = read_elst(&mut stream, &header).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"elst"));
|
|
assert_eq!(parsed.size, 56);
|
|
assert_eq!(parsed.edits.len(), 2);
|
|
assert_eq!(parsed.edits[1].segment_duration, 72623859723010820);
|
|
assert_eq!(parsed.edits[1].media_time, 361984551075317512);
|
|
assert_eq!(parsed.edits[1].media_rate_integer, 2314);
|
|
assert_eq!(parsed.edits[1].media_rate_fraction, 2828);
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_mdhd_v0() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 32]; // size
|
|
write!(&mut test, "mdhd").unwrap(); // type
|
|
test.extend(vec![0, 0, 0, 0]); // fullbox
|
|
test.extend(vec![0, 0, 0, 0,
|
|
0, 0, 0, 0,
|
|
1, 2, 3, 4,
|
|
5, 6, 7, 8,
|
|
0, 0, 0, 0]);
|
|
assert_eq!(test.len(), 32);
|
|
|
|
let mut stream = Cursor::new(test);
|
|
let header = read_box_header(&mut stream).unwrap();
|
|
let parsed = read_mdhd(&mut stream, &header).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"mdhd"));
|
|
assert_eq!(parsed.size, 32);
|
|
assert_eq!(parsed.timescale, 16909060);
|
|
assert_eq!(parsed.duration, 84281096);
|
|
println!("box {:?}", parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_mdhd_v1() {
|
|
use std::io::{Cursor, Write};
|
|
let mut test: Vec<u8> = vec![0, 0, 0, 44]; // size
|
|
write!(&mut test, "mdhd").unwrap(); // type
|
|
test.extend(vec![1, 0, 0, 0]); // fullbox
|
|
test.extend(vec![0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
1, 2, 3, 4,
|
|
5, 6, 7, 8, 5, 6, 7, 8,
|
|
0, 0, 0, 0]);
|
|
assert_eq!(test.len(), 44);
|
|
|
|
let mut stream = Cursor::new(test);
|
|
let header = read_box_header(&mut stream).unwrap();
|
|
let parsed = read_mdhd(&mut stream, &header).unwrap();
|
|
assert_eq!(parsed.name, FourCC(*b"mdhd"));
|
|
assert_eq!(parsed.size, 44);
|
|
assert_eq!(parsed.timescale, 16909060);
|
|
assert_eq!(parsed.duration, 361984551075317512);
|
|
println!("box {:?}", parsed);
|
|
}
|