mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-13 21:12:55 -04:00
fix live view
This broke with the media vs wall duration split, part of #34.
This commit is contained in:
parent
036e8427e6
commit
b9c08b18a4
2
db/db.rs
2
db/db.rs
@ -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)]
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
12
db/writer.rs
12
db/writer.rs
@ -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;
|
||||
{
|
||||
|
@ -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
|
||||
|
75
src/mp4.rs
75
src/mp4.rs
@ -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",
|
||||
|
32
src/web.rs
32
src/web.rs
@ -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)?;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user