mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-26 23:25:55 -05:00
improve the camera html page
* sort by newest recording first (even if time jumps backwards), which seems more useful / less confusing. * add a trim=true URL parameter to trim the .mp4s to not extend beyond the range in question. Otherwise it's quite difficult to produce such a URL in the new s= format: you'd have to manually inspect the database to find the precise start time of the recording and do the math by hand.
This commit is contained in:
parent
068890fa8a
commit
0f4c554ec5
34
src/error.rs
34
src/error.rs
@ -40,6 +40,7 @@ use serde_json;
|
|||||||
use std::boxed::Box;
|
use std::boxed::Box;
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
use std::error;
|
use std::error;
|
||||||
|
use std::error::Error as E;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::result;
|
use std::result;
|
||||||
@ -92,56 +93,49 @@ impl fmt::Display for Error {
|
|||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
impl From<rusqlite::Error> for Error {
|
||||||
fn from(err: rusqlite::Error) -> Self {
|
fn from(err: rusqlite::Error) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: String::from(err.description()), cause: Some(Box::new(err))}
|
||||||
Error{description: String::from(err.description()),
|
}
|
||||||
cause: Some(Box::new(err))}
|
}
|
||||||
|
|
||||||
|
impl From<fmt::Error> for Error {
|
||||||
|
fn from(err: fmt::Error) -> Self {
|
||||||
|
Error{description: String::from(err.description()), cause: Some(Box::new(err))}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Self {
|
fn from(err: io::Error) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: String::from(err.description()), cause: Some(Box::new(err))}
|
||||||
Error{description: String::from(err.description()),
|
|
||||||
cause: Some(Box::new(err))}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<time::ParseError> for Error {
|
impl From<time::ParseError> for Error {
|
||||||
fn from(err: time::ParseError) -> Self {
|
fn from(err: time::ParseError) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: String::from(err.description()), cause: Some(Box::new(err))}
|
||||||
Error{description: String::from(err.description()),
|
|
||||||
cause: Some(Box::new(err))}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<num::ParseIntError> for Error {
|
impl From<num::ParseIntError> for Error {
|
||||||
fn from(err: num::ParseIntError) -> Self {
|
fn from(err: num::ParseIntError) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: err.description().to_owned(), cause: Some(Box::new(err))}
|
||||||
Error{description: err.description().to_owned(),
|
|
||||||
cause: Some(Box::new(err))}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
impl From<serde_json::Error> for Error {
|
||||||
fn from(err: serde_json::Error) -> Self {
|
fn from(err: serde_json::Error) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: format!("{} ({})", err.description(), err), cause: Some(Box::new(err))}
|
||||||
Error{description: format!("{} ({})", err.description(), err),
|
|
||||||
cause: Some(Box::new(err))}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ffmpeg::Error> for Error {
|
impl From<ffmpeg::Error> for Error {
|
||||||
fn from(err: ffmpeg::Error) -> Self {
|
fn from(err: ffmpeg::Error) -> Self {
|
||||||
use std::error::{Error as E};
|
Error{description: format!("{} ({})", err.description(), err), cause: Some(Box::new(err))}
|
||||||
Error{description: format!("{} ({})", err.description(), err),
|
|
||||||
cause: Some(Box::new(err))}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<uuid::ParseError> for Error {
|
impl From<uuid::ParseError> for Error {
|
||||||
fn from(_: uuid::ParseError) -> Self {
|
fn from(_: uuid::ParseError) -> Self {
|
||||||
Error{description: String::from("UUID parse error"),
|
Error{description: String::from("UUID parse error"), cause: None}
|
||||||
cause: None}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
48
src/web.rs
48
src/web.rs
@ -308,7 +308,21 @@ impl Handler {
|
|||||||
|
|
||||||
fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, query: &str,
|
fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, query: &str,
|
||||||
uuid: Uuid) -> Result<Vec<u8>, Error> {
|
uuid: Uuid) -> Result<Vec<u8>, Error> {
|
||||||
let r = Handler::get_optional_range(query)?;
|
let (r, trim) = {
|
||||||
|
let mut start = i64::min_value();
|
||||||
|
let mut end = i64::max_value();
|
||||||
|
let mut trim = false;
|
||||||
|
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||||
|
let (key, value) = (key.borrow(), value.borrow());
|
||||||
|
match key {
|
||||||
|
"start_time_90k" => start = i64::from_str(value)?,
|
||||||
|
"end_time_90k" => end = i64::from_str(value)?,
|
||||||
|
"trim" if value == "true" => trim = true,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(recording::Time(start) .. recording::Time(end), trim)
|
||||||
|
};
|
||||||
let camera = db.get_camera(uuid)
|
let camera = db.get_camera(uuid)
|
||||||
.ok_or_else(|| Error::new("no such camera".to_owned()))?;
|
.ok_or_else(|| Error::new("no such camera".to_owned()))?;
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
@ -338,17 +352,41 @@ impl Handler {
|
|||||||
static FORCE_SPLIT_DURATION: recording::Duration =
|
static FORCE_SPLIT_DURATION: recording::Duration =
|
||||||
recording::Duration(60 * 60 * recording::TIME_UNITS_PER_SEC);
|
recording::Duration(60 * 60 * recording::TIME_UNITS_PER_SEC);
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
db.list_aggregated_recordings(camera.id, r, FORCE_SPLIT_DURATION, |row| {
|
db.list_aggregated_recordings(camera.id, r.clone(), FORCE_SPLIT_DURATION, |row| {
|
||||||
rows.push(row.clone());
|
rows.push(row.clone());
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
rows.sort_by(|r1, r2| r1.time.start.cmp(&r2.time.start));
|
|
||||||
|
// Display newest recording first.
|
||||||
|
rows.sort_by(|r1, r2| r2.ids.start.cmp(&r1.ids.start));
|
||||||
|
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
let seconds = (row.time.end.0 - row.time.start.0) / recording::TIME_UNITS_PER_SEC;
|
let seconds = (row.time.end.0 - row.time.start.0) / recording::TIME_UNITS_PER_SEC;
|
||||||
|
let url = {
|
||||||
|
let mut url = String::with_capacity(64);
|
||||||
|
use std::fmt::Write;
|
||||||
|
write!(&mut url, "view.mp4?s={}", row.ids.start)?;
|
||||||
|
if row.ids.end != row.ids.start + 1 {
|
||||||
|
write!(&mut url, "-{}", row.ids.end - 1)?;
|
||||||
|
}
|
||||||
|
if trim {
|
||||||
|
let rel_start = if row.time.start < r.start { Some(r.start - row.time.start) }
|
||||||
|
else { None };
|
||||||
|
let rel_end = if row.time.end > r.end { Some(r.end - row.time.start) }
|
||||||
|
else { None };
|
||||||
|
if rel_start.is_some() || rel_end.is_some() {
|
||||||
|
url.push('.');
|
||||||
|
if let Some(s) = rel_start { write!(&mut url, "{}", s.0)?; }
|
||||||
|
url.push('-');
|
||||||
|
if let Some(e) = rel_end { write!(&mut url, "{}", e.0)?; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url
|
||||||
|
};
|
||||||
write!(&mut buf, "\
|
write!(&mut buf, "\
|
||||||
<tr><td><a href=\"view.mp4?s={}-{}\">{}</a></td>\
|
<tr><td><a href=\"{}\">{}</a></td>\
|
||||||
<td>{}</td><td>{}x{}</td><td>{:.0}</td><td>{:b}B</td><td>{}bps</td></tr>\n",
|
<td>{}</td><td>{}x{}</td><td>{:.0}</td><td>{:b}B</td><td>{}bps</td></tr>\n",
|
||||||
row.ids.start, row.ids.end - 1, HumanizedTimestamp(Some(row.time.start)),
|
url, HumanizedTimestamp(Some(row.time.start)),
|
||||||
HumanizedTimestamp(Some(row.time.end)), row.video_sample_entry.width,
|
HumanizedTimestamp(Some(row.time.end)), row.video_sample_entry.width,
|
||||||
row.video_sample_entry.height,
|
row.video_sample_entry.height,
|
||||||
if seconds == 0 { 0. } else { row.video_samples as f32 / seconds as f32 },
|
if seconds == 0 { 0. } else { row.video_samples as f32 / seconds as f32 },
|
||||||
|
Loading…
Reference in New Issue
Block a user