serve 'video/mp4; codecs="avc1.xxxxxx"' mime type

This can be used when constructing a HTML5 SourceBuffer.
This commit is contained in:
Scott Lamb 2017-10-03 23:25:58 -07:00
parent 9eff91f7da
commit 7673a00bd9
7 changed files with 78 additions and 29 deletions

View File

@ -182,7 +182,9 @@ Example response:
### `/camera/<uuid>/view.mp4`
A GET returns a `.mp4` file, with an etag and support for range requests.
A GET returns a `.mp4` file, with an etag and support for range requests. The
MIME type will be `video/mp4`, with a `codecs` parameter as specified in [RFC
6381][rfc-6381].
Expected query parameters:
@ -227,7 +229,8 @@ TODO: error behavior on missing segment. It should be a 404, likely with an
### `/camera/<uuid>/view.m4s`
A GET returns a `.mp4` suitable for use as a [HTML5 Media Source Extensions
media segment][media-segment].
media segment][media-segment]. The MIME type will be `video/mp4`, with a
`codecs` parameter as specified in [RFC 6381][rfc-6381].
Expected query parameters:
@ -252,11 +255,8 @@ recording segment for several reasons:
### `/init/<sha1>.mp4`
A GET returns a `.mp4` suitable for use as a [HTML5 Media Source Extensions
initialization segment][init-segment].
TODO: return a MIME type with a `Codecs` parameter as in [RFC 6381](rfc-6381). Web
browsers expect this parameter when initializing a `SourceBuffer`; currently
the user agent must divine this information.
initialization segment][init-segment]. The MIME type will be `video/mp4`, with
a `codecs` parameter as specified in [RFC 6381][rfc-6381].
[media-segment]: https://w3c.github.io/media-source/isobmff-byte-stream-format.html#iso-media-segments
[init-segment]: https://w3c.github.io/media-source/isobmff-byte-stream-format.html#iso-init-segments

View File

@ -53,6 +53,7 @@
use error::{Error, ResultExt};
use fnv;
use h264;
use lru_cache::LruCache;
use openssl::hash;
use parking_lot::{Mutex,MutexGuard};
@ -221,11 +222,12 @@ impl rusqlite::types::FromSql for PlaybackData {
/// the codec, width, height, etc.
#[derive(Debug)]
pub struct VideoSampleEntry {
pub data: Vec<u8>,
pub rfc6381_codec: String,
pub id: i32,
pub width: u16,
pub height: u16,
pub sha1: [u8; 20],
pub data: Vec<u8>,
}
/// A row used in `list_recordings_by_time` and `list_recordings_by_id`.
@ -1077,12 +1079,18 @@ impl LockedDatabase {
id, sha1_vec.len())));
}
sha1.copy_from_slice(&sha1_vec);
self.state.video_sample_entries.insert(id, Arc::new(VideoSampleEntry{
let data: Vec<u8> = row.get_checked(4)?;
// TODO: store this in the database rather than have codec-specific dispatch logic here.
let rfc6381_codec = h264::rfc6381_codec_from_sample_entry(&data)?;
self.state.video_sample_entries.insert(id, Arc::new(VideoSampleEntry {
id: id as i32,
width: row.get_checked::<_, i32>(2)? as u16,
height: row.get_checked::<_, i32>(3)? as u16,
sha1: sha1,
data: row.get_checked(4)?,
sha1,
data,
rfc6381_codec,
}));
}
info!("Loaded {} video sample entries",
@ -1140,8 +1148,9 @@ impl LockedDatabase {
/// Inserts the specified video sample entry if absent.
/// On success, returns the id of a new or existing row.
pub fn insert_video_sample_entry(&mut self, w: u16, h: u16, data: &[u8]) -> Result<i32, Error> {
let sha1 = hash::hash2(hash::MessageDigest::sha1(), data)?;
pub fn insert_video_sample_entry(&mut self, w: u16, h: u16, data: Vec<u8>,
rfc6381_codec: String) -> Result<i32, Error> {
let sha1 = hash::hash2(hash::MessageDigest::sha1(), &data)?;
let mut sha1_bytes = [0u8; 20];
sha1_bytes.copy_from_slice(&sha1);
@ -1168,12 +1177,13 @@ impl LockedDatabase {
])?;
let id = self.conn.last_insert_rowid() as i32;
self.state.video_sample_entries.insert(id, Arc::new(VideoSampleEntry{
self.state.video_sample_entries.insert(id, Arc::new(VideoSampleEntry {
id: id,
width: w,
height: h,
sha1: sha1_bytes,
data: data.to_vec(),
data: data,
rfc6381_codec,
}));
Ok(id)
@ -1485,6 +1495,7 @@ mod tests {
assert_eq!(r.video_samples, row.video_samples);
assert_eq!(r.video_sync_samples, row.video_sync_samples);
assert_eq!(r.sample_file_bytes, row.sample_file_bytes);
assert_eq!(row.video_sample_entry.rfc6381_codec, "avc1.4d0029");
Ok(())
}).unwrap();
}
@ -1628,7 +1639,9 @@ mod tests {
assert_unsorted_eq(db.lock().list_reserved_sample_files().unwrap(),
vec![uuid_to_use, uuid_to_keep]);
let vse_id = db.lock().insert_video_sample_entry(768, 512, &[0u8; 100]).unwrap();
let vse_id = db.lock().insert_video_sample_entry(
1920, 1080, include_bytes!("testdata/avc1").to_vec(),
"avc1.4d0029".to_owned()).unwrap();
assert!(vse_id > 0, "vse_id = {}", vse_id);
// Inserting a recording should succeed and remove its uuid from the reserved table.

View File

@ -95,6 +95,7 @@ fn parse_annex_b_extra_data(data: &[u8]) -> Result<(&[u8], &[u8])> {
#[derive(Debug, PartialEq, Eq)]
pub struct ExtraData {
pub sample_entry: Vec<u8>,
pub rfc6381_codec: String,
pub width: u16,
pub height: u16,
@ -134,7 +135,7 @@ impl ExtraData {
// This is a concatenation of the following boxes/classes.
// SampleEntry, ISO/IEC 14496-10 section 8.5.2.
// SampleEntry, ISO/IEC 14496-12 section 8.5.2.
let avc1_len_pos = sample_entry.len();
sample_entry.write_u32::<BigEndian>(avc1_len as u32)?; // length
// type + reserved + data_reference_index = 1
@ -215,15 +216,28 @@ impl ExtraData {
length {}", avc1_len, sample_entry.len() - avc1_len_pos,
avc_decoder_config_len)));
}
Ok(ExtraData{
sample_entry: sample_entry,
width: width,
height: height,
need_transform: need_transform,
let rfc6381_codec = rfc6381_codec_from_sample_entry(&sample_entry)?;
Ok(ExtraData {
sample_entry,
rfc6381_codec,
width,
height,
need_transform,
})
}
}
pub fn rfc6381_codec_from_sample_entry(sample_entry: &[u8]) -> Result<String> {
if sample_entry.len() < 99 || &sample_entry[4..8] != b"avc1" ||
&sample_entry[90..94] != b"avcC" {
return Err(Error::new("not a valid AVCSampleEntry".to_owned()));
}
let profile_idc = sample_entry[103];
let constraint_flags_byte = sample_entry[104];
let level_idc = sample_entry[105];
Ok(format!("avc1.{:02x}{:02x}{:02x}", profile_idc, constraint_flags_byte, level_idc))
}
/// Transforms sample data from Annex B format to AVC format. Should be called on samples iff
/// `ExtraData::need_transform` is true. Uses an out parameter `avc_sample` rather than a return
/// so that memory allocations can be reused from sample to sample.
@ -245,6 +259,8 @@ pub fn transform_sample_data(annexb_sample: &[u8], avc_sample: &mut Vec<u8>) ->
#[cfg(test)]
mod tests {
use testutil;
const ANNEX_B_TEST_INPUT: [u8; 35] = [
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
@ -283,6 +299,7 @@ mod tests {
#[test]
fn test_decode() {
testutil::init();
let data = &ANNEX_B_TEST_INPUT;
let mut pieces = Vec::new();
super::decode_h264_annex_b(data, |p| {
@ -294,23 +311,28 @@ mod tests {
#[test]
fn test_sample_entry_from_avc_decoder_config() {
testutil::init();
let e = super::ExtraData::parse(&AVC_DECODER_CONFIG_TEST_INPUT, 1280, 720).unwrap();
assert_eq!(&e.sample_entry[..], &TEST_OUTPUT[..]);
assert_eq!(e.width, 1280);
assert_eq!(e.height, 720);
assert_eq!(e.need_transform, false);
assert_eq!(e.rfc6381_codec, "avc1.4d001f");
}
#[test]
fn test_sample_entry_from_annex_b() {
testutil::init();
let e = super::ExtraData::parse(&ANNEX_B_TEST_INPUT, 1280, 720).unwrap();
assert_eq!(e.width, 1280);
assert_eq!(e.height, 720);
assert_eq!(e.need_transform, true);
assert_eq!(e.rfc6381_codec, "avc1.4d001f");
}
#[test]
fn test_transform_sample_data() {
testutil::init();
const INPUT: [u8; 64] = [
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,

View File

@ -1472,8 +1472,19 @@ impl http_entity::Entity for File {
type Body = slices::Body;
fn add_headers(&self, hdrs: &mut header::Headers) {
// TODO: add RFC 6381 "Codecs" parameter.
hdrs.set(header::ContentType("video/mp4".parse().unwrap()));
let mut mime = String::with_capacity(64);
mime.push_str("video/mp4; codecs=\"");
let mut first = true;
for e in &self.0.video_sample_entries {
if first {
first = false
} else {
mime.push_str(", ");
}
mime.push_str(&e.rfc6381_codec);
}
mime.push('"');
hdrs.set(header::ContentType(mime.parse().unwrap()));
}
fn last_modified(&self) -> Option<header::HttpDate> { Some(self.0.last_modified) }
fn etag(&self) -> Option<header::EntityTag> { Some(self.0.etag.clone()) }
@ -1725,7 +1736,8 @@ mod tests {
const START_TIME: recording::Time = recording::Time(1430006400i64 * TIME_UNITS_PER_SEC);
let extra_data = input.get_extra_data().unwrap();
let video_sample_entry_id = db.db.lock().insert_video_sample_entry(
extra_data.width, extra_data.height, &extra_data.sample_entry).unwrap();
extra_data.width, extra_data.height, extra_data.sample_entry,
extra_data.rfc6381_codec).unwrap();
let mut output = db.dir.create_writer(&db.syncer_channel, None,
TEST_CAMERA_ID, video_sample_entry_id).unwrap();

View File

@ -117,7 +117,8 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks, S: 'a + stream::Stream {
let extra_data = stream.get_extra_data()?;
let video_sample_entry_id =
self.db.lock().insert_video_sample_entry(extra_data.width, extra_data.height,
&extra_data.sample_entry)?;
extra_data.sample_entry,
extra_data.rfc6381_codec)?;
debug!("{}: video_sample_entry_id={}", self.short_name, video_sample_entry_id);
let mut seen_key_frame = false;
let mut state: Option<WriterState> = None;

BIN
src/testdata/avc1 vendored Normal file

Binary file not shown.

View File

@ -118,8 +118,8 @@ impl TestDb {
pub fn create_recording_from_encoder(&self, encoder: recording::SampleIndexEncoder)
-> db::ListRecordingsRow {
let mut db = self.db.lock();
let video_sample_entry_id =
db.insert_video_sample_entry(1920, 1080, &[0u8; 100]).unwrap();
let video_sample_entry_id = db.insert_video_sample_entry(
1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap();
{
let mut tx = db.tx().unwrap();
tx.bypass_reservation_for_testing = true;
@ -155,7 +155,8 @@ pub fn add_dummy_recordings_to_db(db: &db::Database, num: usize) {
let mut data = Vec::new();
data.extend_from_slice(include_bytes!("testdata/video_sample_index.bin"));
let mut db = db.lock();
let video_sample_entry_id = db.insert_video_sample_entry(1920, 1080, &[0u8; 100]).unwrap();
let video_sample_entry_id = db.insert_video_sample_entry(
1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap();
const START_TIME: recording::Time = recording::Time(1430006400i64 * TIME_UNITS_PER_SEC);
const DURATION: recording::Duration = recording::Duration(5399985);
let mut recording = db::RecordingToInsert{