2016-11-25 14:34:00 -08:00
|
|
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
|
|
|
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// In addition, as a special exception, the copyright holders give
|
|
|
|
// permission to link the code of portions of this program with the
|
|
|
|
// OpenSSL library under certain conditions as described in each
|
|
|
|
// individual source file, and distribute linked combinations including
|
|
|
|
// the two.
|
|
|
|
//
|
|
|
|
// You must obey the GNU General Public License in all respects for all
|
|
|
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
|
|
// exception, you may extend this exception to your version of the
|
|
|
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
|
|
// so, delete this exception statement from your version. If you delete
|
|
|
|
// this exception statement from all source files in the program, then
|
|
|
|
// also delete it here.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
extern crate hyper;
|
|
|
|
|
2018-03-30 08:53:59 -07:00
|
|
|
use base::strutil;
|
2016-11-25 14:34:00 -08:00
|
|
|
use core::borrow::Borrow;
|
|
|
|
use core::str::FromStr;
|
2018-02-20 23:15:39 -08:00
|
|
|
use db::{self, recording};
|
|
|
|
use db::dir::SampleFileDir;
|
2018-02-20 22:46:14 -08:00
|
|
|
use failure::Error;
|
2018-02-11 22:45:51 -08:00
|
|
|
use fnv::FnvHashMap;
|
2017-03-02 19:29:28 -08:00
|
|
|
use futures::{future, stream};
|
2017-10-21 21:54:27 -07:00
|
|
|
use futures_cpupool;
|
2017-02-05 20:13:51 -08:00
|
|
|
use json;
|
2018-01-23 11:08:21 -08:00
|
|
|
use http_serve;
|
2018-03-03 06:43:36 -08:00
|
|
|
use hyper::header::{self, Header};
|
2017-03-02 19:29:28 -08:00
|
|
|
use hyper::server::{self, Request, Response};
|
2016-11-25 14:34:00 -08:00
|
|
|
use mime;
|
|
|
|
use mp4;
|
2017-03-02 19:29:28 -08:00
|
|
|
use reffers::ARefs;
|
2016-12-20 22:08:18 -08:00
|
|
|
use regex::Regex;
|
2016-11-25 14:34:00 -08:00
|
|
|
use serde_json;
|
2017-03-02 19:29:28 -08:00
|
|
|
use slices;
|
2017-10-21 21:54:27 -07:00
|
|
|
use std::collections::HashMap;
|
2016-12-20 22:08:18 -08:00
|
|
|
use std::cmp;
|
2017-10-21 21:54:27 -07:00
|
|
|
use std::fs;
|
2016-12-08 21:28:50 -08:00
|
|
|
use std::ops::Range;
|
2017-10-21 21:54:27 -07:00
|
|
|
use std::path::PathBuf;
|
2017-02-24 21:33:26 -08:00
|
|
|
use std::sync::Arc;
|
2016-11-25 14:34:00 -08:00
|
|
|
use url::form_urlencoded;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
lazy_static! {
|
2016-12-20 22:08:18 -08:00
|
|
|
/// Regex used to parse the `s` query parameter to `view.mp4`.
|
|
|
|
/// As described in `design/api.md`, this is of the form
|
2018-03-02 11:38:11 -08:00
|
|
|
/// `START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`.
|
|
|
|
static ref SEGMENTS_RE: Regex =
|
|
|
|
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
enum Path {
|
2018-01-23 11:05:07 -08:00
|
|
|
TopLevel, // "/api/"
|
|
|
|
InitSegment([u8; 20]), // "/api/init/<sha1>.mp4"
|
|
|
|
Camera(Uuid), // "/api/cameras/<uuid>/"
|
|
|
|
StreamRecordings(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/recordings"
|
|
|
|
StreamViewMp4(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/view.mp4"
|
|
|
|
StreamViewMp4Segment(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/view.m4s"
|
|
|
|
Static, // "<other path>"
|
2016-11-25 14:34:00 -08:00
|
|
|
NotFound,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn decode_path(path: &str) -> Path {
|
2017-10-21 21:54:27 -07:00
|
|
|
if !path.starts_with("/api/") {
|
|
|
|
return Path::Static;
|
|
|
|
}
|
|
|
|
let path = &path["/api".len()..];
|
2016-11-25 14:34:00 -08:00
|
|
|
if path == "/" {
|
2017-10-21 21:54:27 -07:00
|
|
|
return Path::TopLevel;
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2017-10-01 15:29:22 -07:00
|
|
|
if path.starts_with("/init/") {
|
|
|
|
if path.len() != 50 || !path.ends_with(".mp4") {
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
|
|
|
if let Ok(sha1) = strutil::dehex(&path.as_bytes()[6..46]) {
|
|
|
|
return Path::InitSegment(sha1);
|
|
|
|
}
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
if !path.starts_with("/cameras/") {
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
|
|
|
let path = &path["/cameras/".len()..];
|
|
|
|
let slash = match path.find('/') {
|
|
|
|
None => { return Path::NotFound; },
|
|
|
|
Some(s) => s,
|
|
|
|
};
|
2018-01-23 11:05:07 -08:00
|
|
|
let uuid = &path[0 .. slash];
|
|
|
|
let path = &path[slash+1 .. ];
|
2016-11-25 14:34:00 -08:00
|
|
|
|
|
|
|
// TODO(slamb): require uuid to be in canonical format.
|
|
|
|
let uuid = match Uuid::parse_str(uuid) {
|
|
|
|
Ok(u) => u,
|
|
|
|
Err(_) => { return Path::NotFound },
|
|
|
|
};
|
2018-01-23 11:05:07 -08:00
|
|
|
|
|
|
|
if path.is_empty() {
|
|
|
|
return Path::Camera(uuid);
|
|
|
|
}
|
|
|
|
|
|
|
|
let slash = match path.find('/') {
|
|
|
|
None => { return Path::NotFound; },
|
|
|
|
Some(s) => s,
|
|
|
|
};
|
|
|
|
let (type_, path) = path.split_at(slash);
|
|
|
|
|
|
|
|
let type_ = match db::StreamType::parse(type_) {
|
|
|
|
None => { return Path::NotFound; },
|
|
|
|
Some(t) => t,
|
|
|
|
};
|
2016-11-25 14:34:00 -08:00
|
|
|
match path {
|
2018-01-23 11:05:07 -08:00
|
|
|
"/recordings" => Path::StreamRecordings(uuid, type_),
|
|
|
|
"/view.mp4" => Path::StreamViewMp4(uuid, type_),
|
|
|
|
"/view.m4s" => Path::StreamViewMp4Segment(uuid, type_),
|
2016-11-25 14:34:00 -08:00
|
|
|
_ => Path::NotFound,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
|
|
struct Segments {
|
|
|
|
ids: Range<i32>,
|
2018-03-02 11:38:11 -08:00
|
|
|
open_id: Option<u32>,
|
2016-12-20 22:08:18 -08:00
|
|
|
start_time: i64,
|
|
|
|
end_time: Option<i64>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Segments {
|
|
|
|
pub fn parse(input: &str) -> Result<Segments, ()> {
|
|
|
|
let caps = SEGMENTS_RE.captures(input).ok_or(())?;
|
2017-01-12 23:09:02 -08:00
|
|
|
let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
|
|
|
|
let ids_end = match caps.get(2) {
|
2018-03-02 11:38:11 -08:00
|
|
|
Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?,
|
2016-12-20 22:08:18 -08:00
|
|
|
None => ids_start,
|
|
|
|
} + 1;
|
2018-03-02 11:38:11 -08:00
|
|
|
let open_id = match caps.get(3) {
|
|
|
|
Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?),
|
|
|
|
None => None,
|
|
|
|
};
|
2016-12-20 22:08:18 -08:00
|
|
|
if ids_start < 0 || ids_end <= ids_start {
|
|
|
|
return Err(());
|
|
|
|
}
|
2018-03-02 11:38:11 -08:00
|
|
|
let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
|
2016-12-20 22:08:18 -08:00
|
|
|
if start_time < 0 {
|
|
|
|
return Err(());
|
|
|
|
}
|
2018-03-02 11:38:11 -08:00
|
|
|
let end_time = match caps.get(5) {
|
2016-12-20 22:08:18 -08:00
|
|
|
Some(v) => {
|
2017-01-12 23:09:02 -08:00
|
|
|
let e = i64::from_str(v.as_str()).map_err(|_| ())?;
|
2016-12-20 22:08:18 -08:00
|
|
|
if e <= start_time {
|
|
|
|
return Err(());
|
|
|
|
}
|
|
|
|
Some(e)
|
|
|
|
},
|
|
|
|
None => None
|
|
|
|
};
|
2018-03-02 11:38:11 -08:00
|
|
|
Ok(Segments {
|
2016-12-20 22:08:18 -08:00
|
|
|
ids: ids_start .. ids_end,
|
2018-03-02 11:38:11 -08:00
|
|
|
open_id,
|
|
|
|
start_time,
|
|
|
|
end_time,
|
2016-12-20 22:08:18 -08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-21 21:54:27 -07:00
|
|
|
/// A user interface file (.html, .js, etc).
|
|
|
|
/// The list of files is loaded into the server at startup; this makes path canonicalization easy.
|
|
|
|
/// The files themselves are opened on every request so they can be changed during development.
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct UiFile {
|
|
|
|
mime: mime::Mime,
|
|
|
|
path: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ServiceInner {
|
2017-03-02 19:29:28 -08:00
|
|
|
db: Arc<db::Database>,
|
2018-02-11 22:45:51 -08:00
|
|
|
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
2017-10-21 21:54:27 -07:00
|
|
|
ui_files: HashMap<String, UiFile>,
|
2018-03-03 06:43:36 -08:00
|
|
|
allow_origin: Option<header::AccessControlAllowOrigin>,
|
2017-10-21 21:54:27 -07:00
|
|
|
pool: futures_cpupool::CpuPool,
|
2017-10-21 23:57:13 -07:00
|
|
|
time_zone_name: String,
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
|
|
|
|
2017-10-21 21:54:27 -07:00
|
|
|
impl ServiceInner {
|
2017-03-02 19:29:28 -08:00
|
|
|
fn not_found(&self) -> Result<Response<slices::Body>, Error> {
|
2017-09-21 21:51:58 -07:00
|
|
|
let body: slices::Body = Box::new(stream::once(Ok(ARefs::new(&b"not found"[..]))));
|
2017-03-02 19:29:28 -08:00
|
|
|
Ok(Response::new()
|
2017-09-21 21:51:58 -07:00
|
|
|
.with_status(hyper::StatusCode::NotFound)
|
2017-06-11 12:57:55 -07:00
|
|
|
.with_header(header::ContentType(mime::TEXT_PLAIN))
|
2017-09-21 21:51:58 -07:00
|
|
|
.with_body(body))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2018-01-23 11:22:23 -08:00
|
|
|
fn top_level(&self, req: &Request) -> Result<Response<slices::Body>, Error> {
|
2017-10-21 21:54:27 -07:00
|
|
|
let mut days = false;
|
2018-01-23 11:22:23 -08:00
|
|
|
if let Some(q) = req.uri().query() {
|
2017-10-21 21:54:27 -07:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) : (_, &str) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
|
|
|
"days" => days = value == "true",
|
|
|
|
_ => {},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-23 11:22:23 -08:00
|
|
|
let mut resp = Response::new().with_header(header::ContentType(mime::APPLICATION_JSON));
|
|
|
|
if let Some(mut w) = http_serve::streaming_body(&req, &mut resp).build() {
|
2016-11-25 14:34:00 -08:00
|
|
|
let db = self.db.lock();
|
2018-01-23 11:22:23 -08:00
|
|
|
serde_json::to_writer(&mut w, &json::TopLevel {
|
|
|
|
time_zone_name: &self.time_zone_name,
|
2018-01-23 11:05:07 -08:00
|
|
|
cameras: (&db, days),
|
2018-01-23 11:22:23 -08:00
|
|
|
})?;
|
|
|
|
}
|
|
|
|
Ok(resp)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2018-01-23 11:22:23 -08:00
|
|
|
fn camera(&self, req: &Request, uuid: Uuid) -> Result<Response<slices::Body>, Error> {
|
|
|
|
let mut resp = Response::new().with_header(header::ContentType(mime::APPLICATION_JSON));
|
|
|
|
if let Some(mut w) = http_serve::streaming_body(&req, &mut resp).build() {
|
2016-11-25 14:34:00 -08:00
|
|
|
let db = self.db.lock();
|
2017-10-21 21:54:27 -07:00
|
|
|
let camera = db.get_camera(uuid)
|
2018-02-20 22:46:14 -08:00
|
|
|
.ok_or_else(|| format_err!("no such camera {}", uuid))?;
|
2018-01-23 11:05:07 -08:00
|
|
|
serde_json::to_writer(&mut w, &json::Camera::wrap(camera, &db, true)?)?
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
2018-01-23 11:22:23 -08:00
|
|
|
Ok(resp)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2018-01-23 11:05:07 -08:00
|
|
|
fn stream_recordings(&self, req: &Request, uuid: Uuid, type_: db::StreamType)
|
2017-03-02 19:29:28 -08:00
|
|
|
-> Result<Response<slices::Body>, Error> {
|
2017-10-17 09:00:05 -07:00
|
|
|
let (r, split) = {
|
|
|
|
let mut time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
|
|
|
let mut split = recording::Duration(i64::max_value());
|
2018-01-23 11:22:23 -08:00
|
|
|
if let Some(q) = req.uri().query() {
|
2017-10-17 09:00:05 -07:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
|
|
|
"startTime90k" => time.start = recording::Time::parse(value)?,
|
|
|
|
"endTime90k" => time.end = recording::Time::parse(value)?,
|
|
|
|
"split90k" => split = recording::Duration(i64::from_str(value)?),
|
|
|
|
_ => {},
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
(time, split)
|
|
|
|
};
|
2016-12-08 21:28:50 -08:00
|
|
|
let mut out = json::ListRecordings{recordings: Vec::new()};
|
|
|
|
{
|
|
|
|
let db = self.db.lock();
|
|
|
|
let camera = db.get_camera(uuid)
|
2018-02-20 22:46:14 -08:00
|
|
|
.ok_or_else(|| format_err!("no such camera {}", uuid))?;
|
2018-01-23 11:05:07 -08:00
|
|
|
let stream_id = camera.streams[type_.index()]
|
2018-02-20 22:46:14 -08:00
|
|
|
.ok_or_else(|| format_err!("no such stream {}/{}", uuid, type_))?;
|
2018-02-23 09:19:42 -08:00
|
|
|
db.list_aggregated_recordings(stream_id, r, split, &mut |row| {
|
2017-10-04 06:36:30 -07:00
|
|
|
let end = row.ids.end - 1; // in api, ids are inclusive.
|
2018-03-01 20:59:05 -08:00
|
|
|
let vse = db.video_sample_entries_by_id().get(&row.video_sample_entry_id).unwrap();
|
2017-10-04 00:00:56 -07:00
|
|
|
out.recordings.push(json::Recording {
|
|
|
|
start_id: row.ids.start,
|
2018-03-02 11:38:11 -08:00
|
|
|
end_id: if end == row.ids.start { None } else { Some(end) },
|
2016-12-20 22:08:18 -08:00
|
|
|
start_time_90k: row.time.start.0,
|
|
|
|
end_time_90k: row.time.end.0,
|
2016-12-08 21:28:50 -08:00
|
|
|
sample_file_bytes: row.sample_file_bytes,
|
2018-03-02 11:38:11 -08:00
|
|
|
open_id: row.open_id,
|
|
|
|
first_uncommitted: row.first_uncommitted,
|
2016-12-08 21:28:50 -08:00
|
|
|
video_samples: row.video_samples,
|
2018-03-01 20:59:05 -08:00
|
|
|
video_sample_entry_width: vse.width,
|
|
|
|
video_sample_entry_height: vse.height,
|
|
|
|
video_sample_entry_sha1: strutil::hex(&vse.sha1),
|
2018-03-02 15:40:32 -08:00
|
|
|
growing: row.growing,
|
2016-12-08 21:28:50 -08:00
|
|
|
});
|
|
|
|
Ok(())
|
|
|
|
})?;
|
|
|
|
}
|
2018-01-23 11:22:23 -08:00
|
|
|
let mut resp = Response::new().with_header(header::ContentType(mime::APPLICATION_JSON));
|
|
|
|
if let Some(mut w) = http_serve::streaming_body(&req, &mut resp).build() {
|
|
|
|
serde_json::to_writer(&mut w, &out)?
|
|
|
|
};
|
|
|
|
Ok(resp)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2017-10-01 15:29:22 -07:00
|
|
|
fn init_segment(&self, sha1: [u8; 20], req: &Request) -> Result<Response<slices::Body>, Error> {
|
|
|
|
let mut builder = mp4::FileBuilder::new(mp4::Type::InitSegment);
|
|
|
|
let db = self.db.lock();
|
2018-03-01 20:59:05 -08:00
|
|
|
for ent in db.video_sample_entries_by_id().values() {
|
2017-10-01 15:29:22 -07:00
|
|
|
if ent.sha1 == sha1 {
|
|
|
|
builder.append_video_sample_entry(ent.clone());
|
2018-02-11 22:45:51 -08:00
|
|
|
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())?;
|
2018-01-23 11:08:21 -08:00
|
|
|
return Ok(http_serve::serve(mp4, req));
|
2017-10-01 15:29:22 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
self.not_found()
|
|
|
|
}
|
|
|
|
|
2018-01-23 11:05:07 -08:00
|
|
|
fn stream_view_mp4(&self, req: &Request, uuid: Uuid, stream_type_: db::StreamType,
|
|
|
|
mp4_type_: mp4::Type) -> Result<Response<slices::Body>, Error> {
|
|
|
|
let stream_id = {
|
2016-11-25 14:34:00 -08:00
|
|
|
let db = self.db.lock();
|
2016-12-02 21:46:31 -08:00
|
|
|
let camera = db.get_camera(uuid)
|
2018-02-20 22:46:14 -08:00
|
|
|
.ok_or_else(|| format_err!("no such camera {}", uuid))?;
|
|
|
|
camera.streams[stream_type_.index()]
|
|
|
|
.ok_or_else(|| format_err!("no such stream {}/{}", uuid, stream_type_))?
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
2018-01-23 11:05:07 -08:00
|
|
|
let mut builder = mp4::FileBuilder::new(mp4_type_);
|
|
|
|
if let Some(q) = req.uri().query() {
|
2017-03-02 19:29:28 -08:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
|
|
|
"s" => {
|
|
|
|
let s = Segments::parse(value).map_err(
|
2018-02-20 22:46:14 -08:00
|
|
|
|_| format_err!("invalid s parameter: {}", value))?;
|
2018-01-23 11:05:07 -08:00
|
|
|
debug!("stream_view_mp4: appending s={:?}", s);
|
2017-03-02 19:29:28 -08:00
|
|
|
let mut est_segments = (s.ids.end - s.ids.start) as usize;
|
|
|
|
if let Some(end) = s.end_time {
|
|
|
|
// There should be roughly ceil((end - start) /
|
|
|
|
// desired_recording_duration) recordings in the desired timespan if
|
|
|
|
// there are no gaps or overlap, possibly another for misalignment of
|
|
|
|
// the requested timespan with the rotate offset and another because
|
|
|
|
// rotation only happens at key frames.
|
|
|
|
let ceil_durations = (end - s.start_time +
|
|
|
|
recording::DESIRED_RECORDING_DURATION - 1) /
|
|
|
|
recording::DESIRED_RECORDING_DURATION;
|
|
|
|
est_segments = cmp::min(est_segments, (ceil_durations + 2) as usize);
|
|
|
|
}
|
|
|
|
builder.reserve(est_segments);
|
|
|
|
let db = self.db.lock();
|
|
|
|
let mut prev = None;
|
|
|
|
let mut cur_off = 0;
|
2018-02-23 09:19:42 -08:00
|
|
|
db.list_recordings_by_id(stream_id, s.ids.clone(), &mut |r| {
|
2018-02-20 10:11:10 -08:00
|
|
|
let recording_id = r.id.recording();
|
|
|
|
|
2018-03-02 11:38:11 -08:00
|
|
|
if let Some(o) = s.open_id {
|
|
|
|
if r.open_id != o {
|
|
|
|
bail!("recording {} has open id {}, requested {}",
|
|
|
|
r.id, r.open_id, o);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-02 19:29:28 -08:00
|
|
|
// Check for missing recordings.
|
|
|
|
match prev {
|
2018-02-20 10:11:10 -08:00
|
|
|
None if recording_id == s.ids.start => {},
|
2018-02-20 22:46:14 -08:00
|
|
|
None => bail!("no such recording {}/{}", stream_id, s.ids.start),
|
2018-02-20 10:11:10 -08:00
|
|
|
Some(id) if r.id.recording() != id + 1 => {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("no such recording {}/{}", stream_id, id + 1);
|
2017-03-02 19:29:28 -08:00
|
|
|
},
|
|
|
|
_ => {},
|
|
|
|
};
|
2018-02-20 10:11:10 -08:00
|
|
|
prev = Some(recording_id);
|
2017-03-02 19:29:28 -08:00
|
|
|
|
|
|
|
// Add a segment for the relevant part of the recording, if any.
|
|
|
|
let end_time = s.end_time.unwrap_or(i64::max_value());
|
|
|
|
let d = r.duration_90k as i64;
|
|
|
|
if s.start_time <= cur_off + d && 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 = start as i32 .. end as i32;
|
2018-02-20 10:11:10 -08:00
|
|
|
debug!("...appending recording {} with times {:?} \
|
|
|
|
(out of dur {})", r.id, times, d);
|
2017-03-02 19:29:28 -08:00
|
|
|
builder.append(&db, r, start as i32 .. end as i32)?;
|
|
|
|
} else {
|
2018-02-20 10:11:10 -08:00
|
|
|
debug!("...skipping recording {} dur {}", r.id, d);
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
|
|
|
cur_off += d;
|
|
|
|
Ok(())
|
|
|
|
})?;
|
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
// Check for missing recordings.
|
|
|
|
match prev {
|
2017-03-02 19:29:28 -08:00
|
|
|
Some(id) if s.ids.end != id + 1 => {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("no such recording {}/{}", stream_id, s.ids.end - 1);
|
2017-03-02 19:29:28 -08:00
|
|
|
},
|
|
|
|
None => {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("no such recording {}/{}", stream_id, s.ids.start);
|
2016-12-20 22:08:18 -08:00
|
|
|
},
|
|
|
|
_ => {},
|
|
|
|
};
|
2017-03-02 19:29:28 -08:00
|
|
|
if let Some(end) = s.end_time {
|
|
|
|
if end > cur_off {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("end time {} is beyond specified recordings", end);
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
2016-12-20 22:08:18 -08:00
|
|
|
}
|
2017-03-02 19:29:28 -08:00
|
|
|
},
|
|
|
|
"ts" => builder.include_timestamp_subtitle_track(value == "true"),
|
2018-02-20 22:46:14 -08:00
|
|
|
_ => bail!("parameter {} not understood", key),
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2018-02-11 22:45:51 -08:00
|
|
|
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())?;
|
2018-01-23 11:08:21 -08:00
|
|
|
Ok(http_serve::serve(mp4, req))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2017-10-21 21:54:27 -07:00
|
|
|
|
|
|
|
fn static_file(&self, req: &Request) -> Result<Response<slices::Body>, Error> {
|
|
|
|
let s = match self.ui_files.get(req.uri().path()) {
|
|
|
|
None => { return self.not_found() },
|
|
|
|
Some(s) => s,
|
|
|
|
};
|
|
|
|
let f = fs::File::open(&s.path)?;
|
2018-01-23 11:08:21 -08:00
|
|
|
let e = http_serve::ChunkedReadFile::new(f, Some(self.pool.clone()), s.mime.clone())?;
|
|
|
|
Ok(http_serve::serve(e, &req))
|
2017-10-21 21:54:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Service(Arc<ServiceInner>);
|
|
|
|
|
|
|
|
impl Service {
|
2018-03-24 22:29:40 -07:00
|
|
|
pub fn new(db: Arc<db::Database>, ui_dir: Option<&str>, allow_origin: Option<String>,
|
|
|
|
zone: String) -> Result<Self, Error> {
|
2017-10-21 21:54:27 -07:00
|
|
|
let mut ui_files = HashMap::new();
|
|
|
|
if let Some(d) = ui_dir {
|
|
|
|
Service::fill_ui_files(d, &mut ui_files);
|
|
|
|
}
|
|
|
|
debug!("UI files: {:#?}", ui_files);
|
2018-02-11 22:45:51 -08:00
|
|
|
let dirs_by_stream_id = {
|
|
|
|
let l = db.lock();
|
|
|
|
let mut d =
|
|
|
|
FnvHashMap::with_capacity_and_hasher(l.streams_by_id().len(), Default::default());
|
|
|
|
for (&id, s) in l.streams_by_id().iter() {
|
|
|
|
let dir_id = match s.sample_file_dir_id {
|
|
|
|
Some(d) => d,
|
|
|
|
None => continue,
|
|
|
|
};
|
|
|
|
d.insert(id, l.sample_file_dirs_by_id()
|
|
|
|
.get(&dir_id)
|
|
|
|
.unwrap()
|
2018-02-14 23:10:10 -08:00
|
|
|
.get()?);
|
2018-02-11 22:45:51 -08:00
|
|
|
}
|
|
|
|
Arc::new(d)
|
|
|
|
};
|
2018-03-03 06:43:36 -08:00
|
|
|
let allow_origin = match allow_origin {
|
|
|
|
None => None,
|
|
|
|
Some(o) => Some(header::AccessControlAllowOrigin::parse_header(&header::Raw::from(o))?),
|
|
|
|
};
|
2017-10-21 21:54:27 -07:00
|
|
|
Ok(Service(Arc::new(ServiceInner {
|
|
|
|
db,
|
2018-02-11 22:45:51 -08:00
|
|
|
dirs_by_stream_id,
|
2017-10-21 21:54:27 -07:00
|
|
|
ui_files,
|
2018-03-03 06:43:36 -08:00
|
|
|
allow_origin,
|
2017-10-21 21:54:27 -07:00
|
|
|
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
|
2017-10-21 23:57:13 -07:00
|
|
|
time_zone_name: zone,
|
2017-10-21 21:54:27 -07:00
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn fill_ui_files(dir: &str, files: &mut HashMap<String, UiFile>) {
|
|
|
|
let r = match fs::read_dir(dir) {
|
|
|
|
Ok(r) => r,
|
|
|
|
Err(e) => {
|
|
|
|
warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}",
|
|
|
|
dir, e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
for e in r {
|
|
|
|
let e = match e {
|
|
|
|
Ok(e) => e,
|
|
|
|
Err(e) => {
|
|
|
|
warn!("Error searching UI directory; may be missing files. Error was: {}", e);
|
|
|
|
continue;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
let (p, mime) = match e.file_name().to_str() {
|
|
|
|
Some(n) if n == "index.html" => ("/".to_owned(), mime::TEXT_HTML),
|
|
|
|
Some(n) if n.ends_with(".html") => (format!("/{}", n), mime::TEXT_HTML),
|
2018-03-12 22:11:45 -07:00
|
|
|
Some(n) if n.ends_with(".ico") => (format!("/{}", n),
|
|
|
|
"image/vnd.microsoft.icon".parse().unwrap()),
|
2018-03-10 16:04:37 -08:00
|
|
|
Some(n) if n.ends_with(".js") => (format!("/{}", n), mime::TEXT_JAVASCRIPT),
|
|
|
|
Some(n) if n.ends_with(".map") => (format!("/{}", n), mime::TEXT_JAVASCRIPT),
|
2017-10-21 21:54:27 -07:00
|
|
|
Some(n) if n.ends_with(".png") => (format!("/{}", n), mime::IMAGE_PNG),
|
|
|
|
Some(n) => {
|
|
|
|
warn!("UI directory file {:?} has unknown extension; skipping", n);
|
|
|
|
continue;
|
|
|
|
},
|
|
|
|
None => {
|
|
|
|
warn!("UI directory file {:?} is not a valid UTF-8 string; skipping",
|
|
|
|
e.file_name());
|
|
|
|
continue;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
files.insert(p, UiFile {
|
|
|
|
mime,
|
|
|
|
path: e.path(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2017-03-02 19:29:28 -08:00
|
|
|
impl server::Service for Service {
|
|
|
|
type Request = Request;
|
|
|
|
type Response = Response<slices::Body>;
|
|
|
|
type Error = hyper::Error;
|
|
|
|
type Future = future::FutureResult<Self::Response, Self::Error>;
|
|
|
|
|
|
|
|
fn call(&self, req: Request) -> Self::Future {
|
|
|
|
debug!("request on: {}", req.uri());
|
|
|
|
let res = match decode_path(req.uri().path()) {
|
2017-10-21 21:54:27 -07:00
|
|
|
Path::InitSegment(sha1) => self.0.init_segment(sha1, &req),
|
2018-01-23 11:22:23 -08:00
|
|
|
Path::TopLevel => self.0.top_level(&req),
|
|
|
|
Path::Camera(uuid) => self.0.camera(&req, uuid),
|
2018-01-23 11:05:07 -08:00
|
|
|
Path::StreamRecordings(uuid, type_) => self.0.stream_recordings(&req, uuid, type_),
|
|
|
|
Path::StreamViewMp4(uuid, type_) => {
|
|
|
|
self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::Normal)
|
2017-10-01 15:29:22 -07:00
|
|
|
},
|
2018-01-23 11:05:07 -08:00
|
|
|
Path::StreamViewMp4Segment(uuid, type_) => {
|
|
|
|
self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::MediaSegment)
|
2017-10-01 15:29:22 -07:00
|
|
|
},
|
2017-10-21 21:54:27 -07:00
|
|
|
Path::NotFound => self.0.not_found(),
|
|
|
|
Path::Static => self.0.static_file(&req),
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
2018-03-03 06:43:36 -08:00
|
|
|
let res = if let Some(ref o) = self.0.allow_origin {
|
|
|
|
res.map(|resp| resp.with_header(o.clone()))
|
|
|
|
} else {
|
|
|
|
res
|
|
|
|
};
|
2017-03-02 19:29:28 -08:00
|
|
|
future::result(res.map_err(|e| {
|
|
|
|
error!("error: {}", e);
|
|
|
|
hyper::Error::Incomplete
|
|
|
|
}))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2018-02-20 23:15:39 -08:00
|
|
|
use db::testutil;
|
2017-10-21 21:54:27 -07:00
|
|
|
use super::Segments;
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
#[test]
|
|
|
|
fn test_segments() {
|
|
|
|
testutil::init();
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 0, end_time: None},
|
|
|
|
Segments::parse("1@42").unwrap());
|
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: None},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1.26-").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 26, end_time: None},
|
|
|
|
Segments::parse("1@42.26-").unwrap());
|
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: Some(42)},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1.-42").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: Some(42)},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1.26-42").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: None},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1-5").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: None},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1-5.26-").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: Some(42)},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1-5.-42").unwrap());
|
2018-03-02 11:38:11 -08:00
|
|
|
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: Some(42)},
|
2016-12-20 22:08:18 -08:00
|
|
|
Segments::parse("1-5.26-42").unwrap());
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2017-02-12 20:37:03 -08:00
|
|
|
|
|
|
|
#[cfg(all(test, feature="nightly"))]
|
|
|
|
mod bench {
|
2017-03-02 19:29:28 -08:00
|
|
|
extern crate reqwest;
|
2017-02-12 20:37:03 -08:00
|
|
|
extern crate test;
|
|
|
|
|
2018-02-20 23:15:39 -08:00
|
|
|
use db::testutil::{self, TestDb};
|
2017-02-12 20:37:03 -08:00
|
|
|
use hyper;
|
|
|
|
use self::test::Bencher;
|
2018-02-03 21:56:04 -08:00
|
|
|
use uuid::Uuid;
|
2017-02-12 20:37:03 -08:00
|
|
|
|
|
|
|
struct Server {
|
|
|
|
base_url: String,
|
2018-02-03 21:56:04 -08:00
|
|
|
test_camera_uuid: Uuid,
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Server {
|
|
|
|
fn new() -> Server {
|
2018-03-23 15:16:43 -07:00
|
|
|
let db = TestDb::new(::base::clock::RealClocks {});
|
2018-02-03 21:56:04 -08:00
|
|
|
let test_camera_uuid = db.test_camera_uuid;
|
2017-02-12 20:37:03 -08:00
|
|
|
testutil::add_dummy_recordings_to_db(&db.db, 1440);
|
2017-03-02 19:29:28 -08:00
|
|
|
let (tx, rx) = ::std::sync::mpsc::channel();
|
2017-02-12 20:37:03 -08:00
|
|
|
::std::thread::spawn(move || {
|
2017-03-02 19:29:28 -08:00
|
|
|
let addr = "127.0.0.1:0".parse().unwrap();
|
2018-03-24 22:29:40 -07:00
|
|
|
let service = super::Service::new(db.db.clone(), None, None,
|
2018-03-03 06:43:36 -08:00
|
|
|
"".to_owned()).unwrap();
|
2017-03-02 19:29:28 -08:00
|
|
|
let server = hyper::server::Http::new()
|
2017-10-21 21:54:27 -07:00
|
|
|
.bind(&addr, move || Ok(service.clone()))
|
2017-03-02 19:29:28 -08:00
|
|
|
.unwrap();
|
|
|
|
tx.send(server.local_addr().unwrap()).unwrap();
|
|
|
|
server.run().unwrap();
|
2017-02-12 20:37:03 -08:00
|
|
|
});
|
2017-03-02 19:29:28 -08:00
|
|
|
let addr = rx.recv().unwrap();
|
2018-02-03 21:56:04 -08:00
|
|
|
Server {
|
|
|
|
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
|
|
|
test_camera_uuid,
|
|
|
|
}
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lazy_static! {
|
|
|
|
static ref SERVER: Server = { Server::new() };
|
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
2018-01-23 11:05:07 -08:00
|
|
|
fn serve_stream_recordings(b: &mut Bencher) {
|
2017-02-12 20:37:03 -08:00
|
|
|
testutil::init();
|
|
|
|
let server = &*SERVER;
|
2018-01-23 11:05:07 -08:00
|
|
|
let url = reqwest::Url::parse(&format!("{}/api/cameras/{}/main/recordings", server.base_url,
|
2018-02-03 21:56:04 -08:00
|
|
|
server.test_camera_uuid)).unwrap();
|
2017-02-12 20:37:03 -08:00
|
|
|
let mut buf = Vec::new();
|
2017-11-16 23:01:09 -08:00
|
|
|
let client = reqwest::Client::new();
|
2017-03-03 22:26:29 -08:00
|
|
|
let mut f = || {
|
2017-11-16 23:01:09 -08:00
|
|
|
let mut resp = client.get(url.clone()).send().unwrap();
|
2017-09-21 21:51:58 -07:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::Ok);
|
2017-02-12 20:37:03 -08:00
|
|
|
buf.clear();
|
|
|
|
use std::io::Read;
|
|
|
|
resp.read_to_end(&mut buf).unwrap();
|
2017-03-03 22:26:29 -08:00
|
|
|
};
|
|
|
|
f(); // warm.
|
|
|
|
b.iter(f);
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
}
|