support .mp4 files > 13.25 hours

Use version 1 of the mvhd, tkhd, and mdhd boxes to support 64-bit
durations. 2^32 units / 90,000 units/sec / 60 sec/min / 60 min/hr ~=
13.25 hrs.

Compatibility: looks like Chrome, Firefox, VLC, and ffmepg all support
version 1 with no problem.
This commit is contained in:
Scott Lamb 2019-01-07 00:59:32 -08:00
parent b9e6a6461f
commit 95a8c2e78d
1 changed files with 25 additions and 25 deletions

View File

@ -106,7 +106,7 @@ use std::time::SystemTime;
/// This value should be incremented any time a change is made to this file that causes different /// This value should be incremented any time a change is made to this file that causes different
/// bytes to be output for a particular set of `Mp4Builder` options. Incrementing this value will /// bytes to be output for a particular set of `Mp4Builder` options. Incrementing this value will
/// cause the etag to change as well. /// cause the etag to change as well.
const FORMAT_VERSION: [u8; 1] = [0x05]; const FORMAT_VERSION: [u8; 1] = [0x06];
/// An `ftyp` (ISO/IEC 14496-12 section 4.3 `FileType`) box. /// An `ftyp` (ISO/IEC 14496-12 section 4.3 `FileType`) box.
const NORMAL_FTYP_BOX: &'static [u8] = &[ const NORMAL_FTYP_BOX: &'static [u8] = &[
@ -545,7 +545,7 @@ pub struct FileBuilder {
segments: Vec<Segment>, segments: Vec<Segment>,
video_sample_entries: SmallVec<[Arc<db::VideoSampleEntry>; 1]>, video_sample_entries: SmallVec<[Arc<db::VideoSampleEntry>; 1]>,
next_frame_num: u32, next_frame_num: u32,
duration_90k: u32, duration_90k: u64,
num_subtitle_samples: u32, num_subtitle_samples: u32,
subtitle_co64_pos: Option<usize>, subtitle_co64_pos: Option<usize>,
body: BodyState, body: BodyState,
@ -791,7 +791,7 @@ impl FileBuilder {
}; };
for s in &mut self.segments { for s in &mut self.segments {
let d = &s.s.desired_range_90k; let d = &s.s.desired_range_90k;
self.duration_90k += (d.end - d.start) as u32; self.duration_90k += (d.end - d.start) as u64;
let end = s.s.start + recording::Duration(d.end as i64); let end = s.s.start + recording::Duration(d.end as i64);
max_end = match max_end { max_end = match max_end {
None => Some(end), None => Some(end),
@ -1004,15 +1004,15 @@ impl FileBuilder {
Ok(()) Ok(())
} }
/// Appends a `MovieHeaderBox` version 0 (ISO/IEC 14496-12 section 8.2.2). /// Appends a `MovieHeaderBox` version 1 (ISO/IEC 14496-12 section 8.2.2).
fn append_mvhd(&mut self, creation_ts: u32) -> Result<(), Error> { fn append_mvhd(&mut self, creation_ts: u32) -> Result<(), Error> {
write_length!(self, { write_length!(self, {
self.body.buf.extend_from_slice(b"mvhd\x00\x00\x00\x00"); self.body.buf.extend_from_slice(b"mvhd\x01\x00\x00\x00");
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(TIME_UNITS_PER_SEC as u32); self.body.append_u32(TIME_UNITS_PER_SEC as u32);
let d = self.duration_90k; let d = self.duration_90k;
self.body.append_u32(d); self.body.append_u64(d);
self.body.append_static(StaticBytestring::MvhdJunk)?; self.body.append_static(StaticBytestring::MvhdJunk)?;
let next_track_id = if self.include_timestamp_subtitle_track { 3 } else { 2 }; let next_track_id = if self.include_timestamp_subtitle_track { 3 } else { 2 };
self.body.append_u32(next_track_id); self.body.append_u32(next_track_id);
@ -1047,7 +1047,7 @@ impl FileBuilder {
self.body.append_u32(creation_ts); self.body.append_u32(creation_ts);
self.body.append_u32(1); // track_id self.body.append_u32(1); // track_id
self.body.append_u32(0); // reserved self.body.append_u32(0); // reserved
self.body.append_u32(self.duration_90k); self.body.append_u32(self.duration_90k as u32);
self.body.append_static(StaticBytestring::TkhdJunk)?; self.body.append_static(StaticBytestring::TkhdJunk)?;
let (width, height) = self.video_sample_entries.iter().fold(None, |m, e| { let (width, height) = self.video_sample_entries.iter().fold(None, |m, e| {
@ -1065,12 +1065,12 @@ impl FileBuilder {
fn append_subtitle_tkhd(&mut self, creation_ts: u32) -> Result<(), Error> { fn append_subtitle_tkhd(&mut self, creation_ts: u32) -> Result<(), Error> {
write_length!(self, { write_length!(self, {
// flags 7: track_enabled | track_in_movie | track_in_preview // flags 7: track_enabled | track_in_movie | track_in_preview
self.body.buf.extend_from_slice(b"tkhd\x00\x00\x00\x07"); self.body.buf.extend_from_slice(b"tkhd\x01\x00\x00\x07");
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(2); // track_id self.body.append_u32(2); // track_id
self.body.append_u32(0); // reserved self.body.append_u32(0); // reserved
self.body.append_u32(self.duration_90k); self.body.append_u64(self.duration_90k);
self.body.append_static(StaticBytestring::TkhdJunk)?; self.body.append_static(StaticBytestring::TkhdJunk)?;
self.body.append_u32(0); // width, unused. self.body.append_u32(0); // width, unused.
self.body.append_u32(0); // height, unused. self.body.append_u32(0); // height, unused.
@ -1160,11 +1160,11 @@ impl FileBuilder {
/// or subtitle track. /// or subtitle track.
fn append_mdhd(&mut self, creation_ts: u32) -> Result<(), Error> { fn append_mdhd(&mut self, creation_ts: u32) -> Result<(), Error> {
write_length!(self, { write_length!(self, {
self.body.buf.extend_from_slice(b"mdhd\x00\x00\x00\x00"); self.body.buf.extend_from_slice(b"mdhd\x01\x00\x00\x00");
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(creation_ts); self.body.append_u64(creation_ts as u64);
self.body.append_u32(TIME_UNITS_PER_SEC as u32); self.body.append_u32(TIME_UNITS_PER_SEC as u32);
self.body.append_u32(self.duration_90k); self.body.append_u64(self.duration_90k);
self.body.append_u32(0x55c40000); // language=und + pre_defined self.body.append_u32(0x55c40000); // language=und + pre_defined
}) })
} }
@ -2151,8 +2151,8 @@ mod tests {
// here fails, it can be updated, but the etag must change as well! Otherwise clients may // here fails, it can be updated, but the etag must change as well! Otherwise clients may
// combine ranges from the new format with ranges from the old format. // combine ranges from the new format with ranges from the old format.
let sha1 = digest(&mp4); let sha1 = digest(&mp4);
assert_eq!("1e5331e8371bd97ac3158b3a86494abc87cdc70e", strutil::hex(&sha1[..])); assert_eq!("17376879bcf872dd4ad1197225a32d5473fb0dc6", strutil::hex(&sha1[..]));
const EXPECTED_ETAG: &'static str = "\"04298efb2df0cc45a6cea65dfdf2e817a3b42ca8\""; const EXPECTED_ETAG: &'static str = "\"953dcf1a61debe785d5dec3ae2d3992a819b68ae\"";
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag()); assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
drop(db.syncer_channel); drop(db.syncer_channel);
db.db.lock().clear_on_flush(); db.db.lock().clear_on_flush();
@ -2172,8 +2172,8 @@ mod tests {
// here fails, it can be updated, but the etag must change as well! Otherwise clients may // here fails, it can be updated, but the etag must change as well! Otherwise clients may
// combine ranges from the new format with ranges from the old format. // combine ranges from the new format with ranges from the old format.
let sha1 = digest(&mp4); let sha1 = digest(&mp4);
assert_eq!("de382684a471f178e4e3a163762711b0653bfd83", strutil::hex(&sha1[..])); assert_eq!("1cd90e0b49747cc54c953153d6709f2fb5df6b14", strutil::hex(&sha1[..]));
const EXPECTED_ETAG: &'static str = "\"16a4f6348560c3de0d149675dccba21ef7906be3\""; const EXPECTED_ETAG: &'static str = "\"736655313f10747528a663190517620cdffea6d0\"";
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag()); assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
drop(db.syncer_channel); drop(db.syncer_channel);
db.db.lock().clear_on_flush(); db.db.lock().clear_on_flush();
@ -2193,8 +2193,8 @@ mod tests {
// here fails, it can be updated, but the etag must change as well! Otherwise clients may // here fails, it can be updated, but the etag must change as well! Otherwise clients may
// combine ranges from the new format with ranges from the old format. // combine ranges from the new format with ranges from the old format.
let sha1 = digest(&mp4); let sha1 = digest(&mp4);
assert_eq!("d655945f94e18e6ed88a2322d27522aff6f76403", strutil::hex(&sha1[..])); assert_eq!("49893e3997da6bc625a04b09abf4b1ddbe0bc85d", strutil::hex(&sha1[..]));
const EXPECTED_ETAG: &'static str = "\"80e418b029e81aa195f90aa6b806015a5030e5be\""; const EXPECTED_ETAG: &'static str = "\"e87ed99dea31b7c4d1e9186045abaf5ac3c2d2f8\"";
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag()); assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
drop(db.syncer_channel); drop(db.syncer_channel);
db.db.lock().clear_on_flush(); db.db.lock().clear_on_flush();
@ -2214,8 +2214,8 @@ mod tests {
// here fails, it can be updated, but the etag must change as well! Otherwise clients may // here fails, it can be updated, but the etag must change as well! Otherwise clients may
// combine ranges from the new format with ranges from the old format. // combine ranges from the new format with ranges from the old format.
let sha1 = digest(&mp4); let sha1 = digest(&mp4);
assert_eq!("e0d28ddf08e24575a82657b1ce0b2da73f32fd88", strutil::hex(&sha1[..])); assert_eq!("0615feaa3c50a7889fb0e6842de3bd3d3143bc78", strutil::hex(&sha1[..]));
const EXPECTED_ETAG: &'static str = "\"5bfea0f20108a7c5b77ef1e21d82ef2abc29540f\""; const EXPECTED_ETAG: &'static str = "\"6f0d21a6027b0e444f404a68527dbf5c9a5c1a26\"";
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag()); assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
drop(db.syncer_channel); drop(db.syncer_channel);
db.db.lock().clear_on_flush(); db.db.lock().clear_on_flush();