fix live view

This broke with the media vs wall duration split, part of #34.
This commit is contained in:
Scott Lamb 2020-08-07 10:16:06 -07:00
parent 036e8427e6
commit b9c08b18a4
6 changed files with 104 additions and 99 deletions

View File

@ -521,7 +521,7 @@ pub struct LiveSegment {
/// The pts, relative to the start of the recording, of the start and end of this live segment,
/// in 90kHz units.
pub off_90k: Range<i32>,
pub media_off_90k: Range<i32>,
}
#[derive(Clone, Debug, Default)]

View File

@ -43,25 +43,24 @@ pub const MAX_RECORDING_WALL_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
pub use base::time::Time;
pub use base::time::Duration;
/// Converts from a wall time offset into a recording to a media time offset.
pub fn wall_to_media(wall_off_90k: i32, wall_duration_90k: i32, media_duration_90k: i32) -> i32 {
debug_assert!(wall_off_90k <= wall_duration_90k,
"wall_off_90k={} wall_duration_90k={} media_duration_90k={}",
wall_off_90k, wall_duration_90k, media_duration_90k);
if wall_duration_90k == 0 {
return 0;
/// Converts from a wall time offset into a recording to a media time offset or vice versa.
pub fn rescale(from_off_90k: i32, from_duration_90k: i32, to_duration_90k: i32) -> i32 {
debug_assert!(from_off_90k <= from_duration_90k,
"from_off_90k={} from_duration_90k={} to_duration_90k={}",
from_off_90k, from_duration_90k, to_duration_90k);
if from_duration_90k == 0 {
return 0; // avoid a divide by zero.
}
// The intermediate values here may overflow i32, so use an i64 instead. The max wall
// time is recording::MAX_RECORDING_WALL_DURATION; the max media duration should be
// roughly the same (design limit of 500 ppm correction). The final result should fit
// within i32.
i32::try_from(i64::from(wall_off_90k) *
i64::from(media_duration_90k) /
i64::from(wall_duration_90k))
.map_err(|_| format!("wall_to_media overflow: {} * {} / {} > i32::max_value()",
wall_off_90k, media_duration_90k,
wall_duration_90k))
i32::try_from(i64::from(from_off_90k) *
i64::from(to_duration_90k) /
i64::from(from_duration_90k))
.map_err(|_| format!("rescale overflow: {} * {} / {} > i32::max_value()",
from_off_90k, to_duration_90k, from_duration_90k))
.unwrap()
}

View File

@ -560,7 +560,7 @@ struct InnerWriter<F: FileWriter> {
/// The pts, relative to the start of this segment and in 90kHz units, up until which live
/// segments have been sent out. Initially 0.
completed_live_segment_off_90k: i32,
completed_live_segment_media_off_90k: i32,
hasher: blake3::Hasher,
@ -634,7 +634,7 @@ impl<'a, C: Clocks + Clone, D: DirWriter> Writer<'a, C, D> {
r,
e: recording::SampleIndexEncoder::new(),
id,
completed_live_segment_off_90k: 0,
completed_live_segment_media_off_90k: 0,
hasher: blake3::Hasher::new(),
local_start: recording::Time(i64::max_value()),
unindexed_sample: None,
@ -686,9 +686,9 @@ impl<'a, C: Clocks + Clone, D: DirWriter> Writer<'a, C, D> {
if is_key {
self.db.lock().send_live_segment(self.stream_id, db::LiveSegment {
recording: w.id.recording(),
off_90k: w.completed_live_segment_off_90k .. d,
media_off_90k: w.completed_live_segment_media_off_90k .. d,
}).unwrap();
w.completed_live_segment_off_90k = d;
w.completed_live_segment_media_off_90k = d;
}
}
let mut remaining = pkt;
@ -726,7 +726,7 @@ fn clamp(v: i64, min: i64, max: i64) -> i64 {
}
impl<F: FileWriter> InnerWriter<F> {
/// Returns the total duration of the `RecordingToInsert` (needed for live view path).
/// Returns the total media duration of the `RecordingToInsert` (needed for live view path).
fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool,
pkt_local_time: recording::Time) -> Result<i32, Error> {
let mut l = self.r.lock();
@ -772,7 +772,7 @@ impl<F: FileWriter> InnerWriter<F> {
// This always ends a live segment.
db.lock().send_live_segment(stream_id, db::LiveSegment {
recording: self.id.recording(),
off_90k: self.completed_live_segment_off_90k .. d,
media_off_90k: self.completed_live_segment_media_off_90k .. d,
}).unwrap();
let wall_duration;
{

View File

@ -359,17 +359,17 @@ Expected query parameters:
`START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`. This
specifies *segments* to include. The produced `.mp4` file will be a
concatenation of the segments indicated by all `s` parameters. The ids to
retrieve are as returned by the `/recordings` URL. The *open id* (see
[glossary](glossary.md)) is optional and will be enforced if present; it's
recommended for disambiguation when the requested range includes uncommitted
recordings. The optional start and end times are in 90k units of wall time
and relative to the start of the first specified id. These can be used to
clip the returned segments. Note they can be used to skip over some ids
entirely; this is allowed so that the caller doesn't need to know the start
time of each interior id. If there is no key frame at the desired relative
start time, frames back to the last key frame will be included in the
returned data, and an edit list will instruct the viewer to skip to the
desired start time.
retrieve are as returned by the `/recordings` URL. The *open id* is
optional and will be enforced if present; it's recommended for
disambiguation when the requested range includes uncommitted recordings.
The optional start and end times are in 90k units of wall time and relative
to the start of the first specified id. These can be used to clip the
returned segments. Note they can be used to skip over some ids entirely;
this is allowed so that the caller doesn't need to know the start time of
each interior id. If there is no key frame at the desired relative start
time, frames back to the last key frame will be included in the returned
data, and an edit list will instruct the viewer to skip to the desired
start time.
* `ts` (optional): should be set to `true` to request a subtitle track be
added with human-readable recording timestamps.
@ -449,8 +449,8 @@ this fundamental reason Moonfire NVR makes no effort to make multiple-segment
* There's currently no way to generate an initialization segment for more
than one video sample entry, so a `.m4s` that uses more than one video
sample entry can't be used.
* The `X-Prev-Media-Duration` and `X-Leading-Duration` headers only describe
the first segment.
* The `X-Prev-Media-Duration` and `X-Leading-Media-Duration` headers only
describe the first segment.
Timestamp tracks (see the `ts` parameter to `.mp4` URIs) aren't supported
today. Most likely browser clients will implement timestamp subtitles via
@ -468,9 +468,9 @@ WebSocket headers as described in [RFC 6455][rfc-6455] and (if authentication
is required) the `s` cookie.
The server will send a sequence of binary messages. Each message corresponds
to one run (GOP) of video: a key (IDR) frame and all other frames which depend
on it. These are encoded as a `.mp4` media segment. The following headers will
be included:
to one GOP of video: a key (IDR) frame and all other frames which depend on it.
These are encoded as a `.mp4` media segment. The following headers will be
included:
* `X-Recording-Id`: the open id, a period, and the recording id of the
recording these frames belong to.
@ -478,11 +478,10 @@ be included:
of a second) of the start of the recording. Note that if the recording
is "unanchored" (as described in `GET /api/.../recordings`), the
recording's start time may change before it is completed.
* `X-Prev-Duration`: as in `/.../view.m4s`.
* `X-Prev-Media-Duration`: as in `/.../view.m4s`.
* `X-Runs`: as in `/.../view.m4s`.
* `X-Time-Range`: the relative start and end times of these frames within
the recording, in the same format as `REL_START_TIME` and `REL_END_TIME`
above.
* `X-Media-Time-Range`: the relative media start and end times of these
frames within the recording, as a half-open interval.
Cameras are typically configured to have about one key frame per second, so
there will be one part per second when the stream is working. If the stream is
@ -501,7 +500,8 @@ Example binary message sequence:
Content-Type: video/mp4; codecs="avc1.640028"
X-Recording-Id: 42.5680
X-Recording-Start: 130985461191810
X-Time-Range: 5220058-5400061
X-Prev-Media-Duration: 10000000
X-Media-Time-Range: 5220058-5400061
X-Video-Sample-Entry-Id: 4
binary mp4 data
@ -511,7 +511,8 @@ binary mp4 data
Content-Type: video/mp4; codecs="avc1.640028"
X-Recording-Id: 42.5681
X-Recording-Start: 130985461191822
X-Time-Range: 0-180002
X-Prev-Media-Duration: 10180003
X-Media-Time-Range: 0-180002
X-Video-Sample-Entry-Id: 4
binary mp4 data
@ -521,18 +522,20 @@ binary mp4 data
Content-Type: video/mp4; codecs="avc1.640028"
X-Recording-Id: 42.5681
X-Recording-Start: 130985461191822
X-Time-Range: 180002-360004
X-Prev-Media-Duration: 10360005
X-Media-Time-Range: 180002-360004
X-Video-Sample-Entry-Id: 4
binary mp4 data
```
These segments are exactly the same as ones that can be retrieved at the
If the wall duration and media duration for these recordings are equal, these
segments are exactly the same as the ones that can be retrieved at the
following URLs, respectively:
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5680@42.5220058-5400061`
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5681@42.0-180002`
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5681@42.180002-360004`
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5680@42.5220058-5400061`
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5681@42.0-180002`
* `/api/cameras/fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe/main/view.m4s?s=5681@42.180002-360004`
Note: an earlier version of this API used a `multipart/mixed` segment instead,
compatible with the [multipart-stream-js][multipart-stream-js] library. The

View File

@ -81,7 +81,7 @@ use bytes::{Buf, BytesMut};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use crate::body::{Chunk, BoxedError, wrap_error};
use db::dir;
use db::recording::{self, TIME_UNITS_PER_SEC, wall_to_media};
use db::recording::{self, TIME_UNITS_PER_SEC, rescale};
use futures::Stream;
use futures::stream;
use http;
@ -347,13 +347,12 @@ struct Segment {
recording_wall_duration_90k: i32,
recording_media_duration_90k: i32,
/// The _desired_, _relative_, _wall_ time range covered by this recording.
/// The _desired_, _relative_, _media_ time range covered by this recording.
/// * _desired_: as noted in `recording::Segment`, the _actual_ time range may be somewhat
/// more if there's no key frame at the desired start.
/// * _relative_: relative to `recording_start` rather than absolute timestamps.
/// * _wall_ time: the media time units are in terms of the cameras' clocks. Wall time units
/// differ slightly.
rel_wall_range_90k: Range<i32>,
/// * _media_ time: as described in design/glossary.md and design/time.md.
rel_media_range_90k: Range<i32>,
/// If generated, the `.mp4`-format sample indexes, accessed only through `get_index`:
/// 1. stts: `slice[.. stsz_start]`
@ -382,18 +381,15 @@ impl fmt::Debug for Segment {
unsafe impl Sync for Segment {}
impl Segment {
fn new(db: &db::LockedDatabase, row: &db::ListRecordingsRow, rel_wall_range_90k: Range<i32>,
fn new(db: &db::LockedDatabase, row: &db::ListRecordingsRow, rel_media_range_90k: Range<i32>,
first_frame_num: u32) -> Result<Self, Error> {
let rel_media_range_90k =
wall_to_media(rel_wall_range_90k.start, row.wall_duration_90k, row.media_duration_90k)
..
wall_to_media(rel_wall_range_90k.end, row.wall_duration_90k, row.media_duration_90k);
Ok(Segment {
s: recording::Segment::new(db, row, rel_media_range_90k).err_kind(ErrorKind::Unknown)?,
s: recording::Segment::new(db, row, rel_media_range_90k.clone())
.err_kind(ErrorKind::Unknown)?,
recording_start: row.start,
recording_wall_duration_90k: row.wall_duration_90k,
recording_media_duration_90k: row.media_duration_90k,
rel_wall_range_90k,
rel_media_range_90k,
index: UnsafeCell::new(Err(())),
index_once: Once::new(),
first_frame_num,
@ -401,9 +397,12 @@ impl Segment {
})
}
fn wall(&self, rel_media_90k: i32) -> i32 {
rescale(rel_media_90k, self.recording_media_duration_90k, self.recording_wall_duration_90k)
}
fn media(&self, rel_wall_90k: i32) -> i32 {
db::recording::wall_to_media(rel_wall_90k, self.recording_wall_duration_90k,
self.recording_media_duration_90k)
rescale(rel_wall_90k, self.recording_wall_duration_90k, self.recording_media_duration_90k)
}
fn get_index<'a, F>(&'a self, db: &db::Database, f: F) -> Result<&'a [u8], Error>
@ -467,7 +466,7 @@ impl Segment {
// Doing this after the fact is more efficient than having a condition on every
// iteration.
if let Some((last_start, dur)) = last_start_and_dur {
let min = cmp::min(self.media(self.rel_wall_range_90k.end) - last_start, dur);
let min = cmp::min(self.rel_media_range_90k.end - last_start, dur);
BigEndian::write_u32(&mut stts[8*frame-4 ..], u32::try_from(min).unwrap());
}
}
@ -558,9 +557,10 @@ impl Segment {
// One more thing to do in the terminal case: fix up the final frame's duration.
// Doing this after the fact is more efficient than having a condition on every
// iteration.
BigEndian::write_u32(&mut v[p-8 .. p-4],
cmp::min(self.media(self.rel_wall_range_90k.end) - r.last_start,
r.last_dur) as u32);
BigEndian::write_u32(
&mut v[p-8 .. p-4],
u32::try_from(cmp::min(self.rel_media_range_90k.end - r.last_start, r.last_dur))
.unwrap());
}
Ok(v)
@ -794,10 +794,10 @@ impl FileBuilder {
}
/// Appends a segment for (a subset of) the given recording.
/// `rel_wall_range_90k` is the wall time range within the recording.
/// Eg `0 .. row.wall_duration_90k` means the full recording.
/// `rel_media_range_90k` is the media time range within the recording.
/// Eg `0 .. row.media_duration_90k` means the full recording.
pub fn append(&mut self, db: &db::LockedDatabase, row: db::ListRecordingsRow,
rel_wall_range_90k: Range<i32>) -> Result<(), Error> {
rel_media_range_90k: Range<i32>) -> Result<(), Error> {
if let Some(prev) = self.segments.last() {
if prev.s.have_trailing_zero() {
bail_t!(InvalidArgument,
@ -810,7 +810,7 @@ impl FileBuilder {
self.prev_media_duration_and_cur_runs = row.prev_media_duration_and_runs
.map(|(d, r)| (d, r + if row.open_id == 0 { 1 } else { 0 }));
}
let s = Segment::new(db, &row, rel_wall_range_90k, self.next_frame_num)?;
let s = Segment::new(db, &row, rel_media_range_90k, self.next_frame_num)?;
self.next_frame_num += s.s.frames as u32;
self.segments.push(s);
@ -848,8 +848,7 @@ impl FileBuilder {
Type::MediaSegment => { etag.update(b":media:"); },
};
for s in &mut self.segments {
let wd = &s.rel_wall_range_90k;
let md = s.media(wd.start) .. s.media(wd.end);
let md = &s.rel_media_range_90k;
// Add the media time for this segment. If edit lists are supported (not media
// segments), this shouldn't include the portion they skip.
@ -859,8 +858,8 @@ impl FileBuilder {
};
self.media_duration_90k += u64::try_from(md.end - start).unwrap();
let wall =
s.recording_start + recording::Duration(i64::from(s.rel_wall_range_90k.start)) ..
s.recording_start + recording::Duration(i64::from(s.rel_wall_range_90k.end));
s.recording_start + recording::Duration(i64::from(s.wall(md.start))) ..
s.recording_start + recording::Duration(i64::from(s.wall(md.end)));
max_end = match max_end {
None => Some(wall.end),
Some(v) => Some(cmp::max(v, wall.end)),
@ -881,8 +880,8 @@ impl FileBuilder {
cursor.write_i64::<BigEndian>(s.s.id.0).err_kind(ErrorKind::Internal)?;
cursor.write_i64::<BigEndian>(s.recording_start.0).err_kind(ErrorKind::Internal)?;
cursor.write_u32::<BigEndian>(s.s.open_id).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(wd.start).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(wd.end).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(md.start).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(md.end).err_kind(ErrorKind::Internal)?;
etag.update(cursor.into_inner());
}
let max_end = match max_end {
@ -1162,7 +1161,7 @@ impl FileBuilder {
// key frame. This relationship should hold true:
// actual start <= desired start <= desired end
let actual_start_90k = s.s.actual_start_90k();
let md = s.media(s.rel_wall_range_90k.start) .. s.media(s.rel_wall_range_90k.end);
let md = &s.rel_media_range_90k;
let skip = md.start - actual_start_90k;
let keep = md.end - md.start;
if skip < 0 || keep < 0 {
@ -1325,13 +1324,12 @@ impl FileBuilder {
for s in &self.segments {
// Note desired media range = actual media range for the subtitle track.
// We still need to consider media time vs wall time.
let wr = &s.rel_wall_range_90k;
let start = s.recording_start + recording::Duration(i64::from(wr.start));
let end = s.recording_start + recording::Duration(i64::from(wr.end));
let mr = &s.rel_media_range_90k;
let start = s.recording_start + recording::Duration(i64::from(s.wall(mr.start)));
let end = s.recording_start + recording::Duration(i64::from(s.wall(mr.end)));
let start_next_sec = recording::Time(
start.0 + TIME_UNITS_PER_SEC - (start.0 % TIME_UNITS_PER_SEC));
let mr = s.media(wr.start) .. s.media(wr.end);
if end <= start_next_sec {
// Segment doesn't last past the next second. Just write one entry.
entry_count += 1;
@ -1346,7 +1344,7 @@ impl FileBuilder {
self.body.append_u32(u32::try_from(media_pos - mr.start).unwrap());
// Then there are zero or more "interior" subtitles, one second each. That's
// one second converted from wall to media duration. wall_to_media rounds down,
// one second converted from wall to media duration. rescale rounds down,
// and these errors accumulate, so the final subtitle can be too early by as
// much as (MAX_RECORDING_WALL_DURATION/TIME_UNITS_PER_SEC) time units, or
// roughly 3 ms. We could avoid that by writing a separate entry for each
@ -1563,11 +1561,12 @@ impl FileInner {
fn get_subtitle_sample_data(&self, i: usize, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
let s = &self.segments[i];
let d = &s.rel_wall_range_90k;
let md = &s.rel_media_range_90k;
let wd = s.wall(md.start) .. s.wall(md.end);
let start_sec =
(s.recording_start + recording::Duration(i64::from(d.start))).unix_seconds();
(s.recording_start + recording::Duration(i64::from(wd.start))).unix_seconds();
let end_sec =
(s.recording_start + recording::Duration(i64::from(d.end) + TIME_UNITS_PER_SEC - 1))
(s.recording_start + recording::Duration(i64::from(wd.end) + TIME_UNITS_PER_SEC - 1))
.unix_seconds();
let l = usize::try_from(l).unwrap();
let mut v = Vec::with_capacity(l);
@ -1642,7 +1641,7 @@ impl http_serve::Entity for File {
HeaderValue::try_from(r.to_string()).expect("ints are valid headers"));
}
if let Some(s) = self.0.segments.first() {
let skip = s.media(s.rel_wall_range_90k.start) - s.s.actual_start_90k();
let skip = s.rel_media_range_90k.start - s.s.actual_start_90k();
if skip > 0 {
hdrs.insert(
"X-Leading-Media-Duration",

View File

@ -448,7 +448,7 @@ impl Service {
db.list_recordings_by_id(stream_id, live.recording .. live.recording+1, &mut |r| {
rows += 1;
row = Some(r);
builder.append(&db, r, live.off_90k.clone())?;
builder.append(&db, r, live.media_off_90k.clone())?;
Ok(())
})?;
if rows != 1 {
@ -466,7 +466,7 @@ impl Service {
"Content-Type: {}\r\n\
X-Recording-Start: {}\r\n\
X-Recording-Id: {}.{}\r\n\
X-Time-Range: {}-{}\r\n\
X-Media-Time-Range: {}-{}\r\n\
X-Prev-Media-Duration: {}\r\n\
X-Runs: {}\r\n\
X-Video-Sample-Entry-Id: {}\r\n\r\n",
@ -474,8 +474,8 @@ impl Service {
row.start.0,
open_id,
live.recording,
live.off_90k.start,
live.off_90k.end,
live.media_off_90k.start,
live.media_off_90k.end,
prev_media_duration.0,
prev_runs + if row.run_offset == 0 { 1 } else { 0 },
&row.video_sample_entry_id);
@ -737,23 +737,27 @@ impl Service {
// Add a segment for the relevant part of the recording, if any.
// Note all calculations here are in wall times / wall durations.
let end_time = s.end_time.unwrap_or(i64::max_value());
let d = i64::from(r.wall_duration_90k);
if s.start_time <= cur_off + d && cur_off < end_time {
let wd = i64::from(r.wall_duration_90k);
if s.start_time <= cur_off + wd && cur_off < end_time {
let start = cmp::max(0, s.start_time - cur_off);
let end = cmp::min(d, end_time - cur_off);
let times = i32::try_from(start).unwrap() ..
i32::try_from(end).unwrap();
debug!("...appending recording {} with times {:?} \
(out of dur {})", r.id, times, d);
let end = cmp::min(wd, end_time - cur_off);
let wr = i32::try_from(start).unwrap() ..
i32::try_from(end).unwrap();
debug!("...appending recording {} with wall duration {:?} \
(out of total {})", r.id, wr, wd);
if start_time_for_filename.is_none() {
start_time_for_filename =
Some(r.start + recording::Duration(start));
}
builder.append(&db, r, times)?;
use recording::rescale;
let mr =
rescale(wr.start, r.wall_duration_90k, r.media_duration_90k) ..
rescale(wr.end, r.wall_duration_90k, r.media_duration_90k);
builder.append(&db, r, mr)?;
} else {
debug!("...skipping recording {} dur {}", r.id, d);
debug!("...skipping recording {} wall dur {}", r.id, wd);
}
cur_off += d;
cur_off += wd;
Ok(())
}).map_err(internal_server_err)?;