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:
Scott Lamb 2017-01-01 22:47:26 -08:00
parent 068890fa8a
commit 0f4c554ec5
2 changed files with 57 additions and 25 deletions

View File

@ -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}
} }
} }

View File

@ -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 },