mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 16:03:22 -05:00
serve 'video/mp4; codecs="avc1.xxxxxx"' mime type
This can be used when constructing a HTML5 SourceBuffer.
This commit is contained in:
parent
9eff91f7da
commit
7673a00bd9
@ -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
|
||||
|
31
src/db.rs
31
src/db.rs
@ -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.
|
||||
|
34
src/h264.rs
34
src/h264.rs
@ -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,
|
||||
|
18
src/mp4.rs
18
src/mp4.rs
@ -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();
|
||||
|
||||
|
@ -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
BIN
src/testdata/avc1
vendored
Normal file
Binary file not shown.
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user