mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-12-07 16:22:31 -05:00
schema version 2: support sub streams
This allows each camera to have a main and a sub stream. Previously there was a field in the schema for the sub stream's url, but it didn't do anything. Now you can configure individual retention for main and sub streams. They show up grouped in the UI. No support for upgrading from schema version 1 yet.
This commit is contained in:
@@ -51,14 +51,13 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
||||
let p = siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into();
|
||||
let m = siv.find_id::<views::EditView>("main_rtsp_path").unwrap().get_content().as_str().into();
|
||||
let s = siv.find_id::<views::EditView>("sub_rtsp_path").unwrap().get_content().as_str().into();
|
||||
db::CameraChange{
|
||||
db::CameraChange {
|
||||
short_name: sn,
|
||||
description: d,
|
||||
host: h,
|
||||
username: u,
|
||||
password: p,
|
||||
main_rtsp_path: m,
|
||||
sub_rtsp_path: s,
|
||||
rtsp_paths: [m, s],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +144,20 @@ fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::Sa
|
||||
let typed = siv.find_id::<views::EditView>("confirm").unwrap().get_content();
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
siv.pop_layer(); // deletion confirmation dialog
|
||||
if let Err(e) = dir::lower_retention(dir.clone(),
|
||||
&[dir::NewLimit{camera_id: id, limit: 0}]) {
|
||||
|
||||
let mut zero_limits = Vec::new();
|
||||
{
|
||||
let l = db.lock();
|
||||
for (&stream_id, stream) in l.streams_by_id() {
|
||||
if stream.camera_id == id {
|
||||
zero_limits.push(dir::NewLimit {
|
||||
stream_id,
|
||||
limit: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = dir::lower_retention(dir.clone(), &zero_limits) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete recordings: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
@@ -162,7 +173,6 @@ fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::Sa
|
||||
|
||||
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: i32) {
|
||||
info!("actually_delete call");
|
||||
siv.pop_layer(); // get rid of the add/edit camera dialog.
|
||||
let result = {
|
||||
let mut l = db.lock();
|
||||
@@ -198,14 +208,14 @@ fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "main", &c.main_rtsp_path)
|
||||
press_test(siv, &c, "main", &c.rtsp_paths[0])
|
||||
})))
|
||||
.child("sub_rtsp_path", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new().with_id("sub_rtsp_path").full_width())
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "sub", &c.sub_rtsp_path)
|
||||
press_test(siv, &c, "sub", &c.rtsp_paths[1])
|
||||
})))
|
||||
.min_height(8);
|
||||
let layout = views::LinearLayout::vertical()
|
||||
@@ -214,22 +224,41 @@ fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv
|
||||
.child(views::TextArea::new().with_id("description").min_height(3))
|
||||
.full_width();
|
||||
let mut dialog = views::Dialog::around(layout);
|
||||
let dialog = if let Some(id) = *item {
|
||||
let dialog = if let Some(camera_id) = *item {
|
||||
let l = db.lock();
|
||||
let camera = l.cameras_by_id().get(&id).expect("missing camera");
|
||||
let camera = l.cameras_by_id().get(&camera_id).expect("missing camera");
|
||||
dialog.find_id("uuid", |v: &mut views::TextView| v.set_content(camera.uuid.to_string()))
|
||||
.expect("missing TextView");
|
||||
let bytes = camera.sample_file_bytes;
|
||||
let mut main_rtsp_path = "";
|
||||
let mut sub_rtsp_path = "";
|
||||
let mut bytes = 0;
|
||||
for (_, s) in l.streams_by_id() {
|
||||
if s.camera_id != camera_id { continue; }
|
||||
bytes += s.sample_file_bytes;
|
||||
match s.type_ {
|
||||
db::StreamType::MAIN => main_rtsp_path = &s.rtsp_path,
|
||||
db::StreamType::SUB => sub_rtsp_path = &s.rtsp_path,
|
||||
};
|
||||
}
|
||||
let name = camera.short_name.clone();
|
||||
for &(view_id, content) in &[("short_name", &camera.short_name),
|
||||
("host", &camera.host),
|
||||
("username", &camera.username),
|
||||
("password", &camera.password),
|
||||
("main_rtsp_path", &camera.main_rtsp_path),
|
||||
("sub_rtsp_path", &camera.sub_rtsp_path)] {
|
||||
for &(view_id, content) in &[("short_name", &*camera.short_name),
|
||||
("host", &*camera.host),
|
||||
("username", &*camera.username),
|
||||
("password", &*camera.password),
|
||||
("main_rtsp_path", main_rtsp_path),
|
||||
("sub_rtsp_path", sub_rtsp_path)] {
|
||||
dialog.find_id(view_id, |v: &mut views::EditView| v.set_content(content.to_string()))
|
||||
.expect("missing EditView");
|
||||
}
|
||||
for s in l.streams_by_id().values() {
|
||||
if s.camera_id != camera_id { continue };
|
||||
let id = match s.type_ {
|
||||
db::StreamType::MAIN => "main_rtsp_path",
|
||||
db::StreamType::SUB => "sub_rtsp_path",
|
||||
};
|
||||
dialog.find_id(id, |v: &mut views::EditView| v.set_content(s.rtsp_path.to_string()))
|
||||
.expect("missing EditView");
|
||||
}
|
||||
dialog.find_id("description",
|
||||
|v: &mut views::TextArea| v.set_content(camera.description.to_string()))
|
||||
.expect("missing TextArea");
|
||||
@@ -237,12 +266,12 @@ fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv
|
||||
.button("Edit", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_edit(s, &db, &dir, Some(id))
|
||||
move |s| press_edit(s, &db, &dir, Some(camera_id))
|
||||
})
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_delete(s, &db, &dir, id, name.clone(), bytes)
|
||||
move |s| press_delete(s, &db, &dir, camera_id, name.clone(), bytes)
|
||||
})
|
||||
} else {
|
||||
dialog.title("Add camera")
|
||||
|
||||
@@ -144,7 +144,8 @@ pub fn run() -> Result<(), Error> {
|
||||
move |siv, item| item(&db, &dir, siv)
|
||||
})
|
||||
.item("Edit cameras".to_string(), cameras::add_dialog)
|
||||
.item("Edit retention".to_string(), retention::add_dialog))
|
||||
.item("Edit retention".to_string(), retention::add_dialog)
|
||||
)
|
||||
.button("Quit", |siv| siv.quit())
|
||||
.title("Main menu"));
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use super::{decode_size, encode_size};
|
||||
|
||||
struct Camera {
|
||||
struct Stream {
|
||||
label: String,
|
||||
used: i64,
|
||||
retain: Option<i64>, // None if unparseable
|
||||
@@ -55,15 +55,15 @@ struct Model {
|
||||
total_used: i64,
|
||||
total_retain: i64,
|
||||
errors: isize,
|
||||
cameras: BTreeMap<i32, Camera>,
|
||||
streams: BTreeMap<i32, Stream>,
|
||||
}
|
||||
|
||||
/// Updates the limits in the database. Doesn't delete excess data (if any).
|
||||
fn update_limits_inner(model: &Model) -> Result<(), Error> {
|
||||
let mut db = model.db.lock();
|
||||
let mut tx = db.tx()?;
|
||||
for (&id, camera) in &model.cameras {
|
||||
tx.update_retention(id, camera.retain.unwrap())?;
|
||||
for (&id, stream) in &model.streams {
|
||||
tx.update_retention(id, stream.retain.unwrap())?;
|
||||
}
|
||||
tx.commit()
|
||||
}
|
||||
@@ -77,12 +77,12 @@ fn update_limits(model: &Model, siv: &mut Cursive) {
|
||||
}
|
||||
|
||||
fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str) {
|
||||
info!("on_edit called for id {}", id);
|
||||
debug!("on_edit called for id {}", id);
|
||||
let mut model = model.borrow_mut();
|
||||
let model: &mut Model = &mut *model;
|
||||
let camera = model.cameras.get_mut(&id).unwrap();
|
||||
let stream = model.streams.get_mut(&id).unwrap();
|
||||
let new_value = decode_size(content).ok();
|
||||
let delta = new_value.unwrap_or(0) - camera.retain.unwrap_or(0);
|
||||
let delta = new_value.unwrap_or(0) - stream.retain.unwrap_or(0);
|
||||
let old_errors = model.errors;
|
||||
if delta != 0 {
|
||||
let prev_over = model.total_retain > model.fs_capacity;
|
||||
@@ -91,7 +91,6 @@ fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str)
|
||||
.unwrap()
|
||||
.set_content(encode_size(model.total_retain));
|
||||
let now_over = model.total_retain > model.fs_capacity;
|
||||
info!("now_over: {}", now_over);
|
||||
if now_over != prev_over {
|
||||
model.errors += if now_over { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>("total_ok")
|
||||
@@ -99,13 +98,13 @@ fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str)
|
||||
.set_content(if now_over { "*" } else { " " });
|
||||
}
|
||||
}
|
||||
if new_value.is_none() != camera.retain.is_none() {
|
||||
if new_value.is_none() != stream.retain.is_none() {
|
||||
model.errors += if new_value.is_none() { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>(&format!("{}_ok", id))
|
||||
.unwrap()
|
||||
.set_content(if new_value.is_none() { "*" } else { " " });
|
||||
}
|
||||
camera.retain = new_value;
|
||||
stream.retain = new_value;
|
||||
info!("model.errors = {}", model.errors);
|
||||
if (model.errors == 0) != (old_errors == 0) {
|
||||
info!("toggling change state: errors={}", model.errors);
|
||||
@@ -119,7 +118,7 @@ fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
|
||||
let typed = siv.find_id::<views::EditView>("confirm")
|
||||
.unwrap()
|
||||
.get_content();
|
||||
info!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
|
||||
debug!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
actually_delete(model, siv);
|
||||
} else {
|
||||
@@ -132,8 +131,8 @@ fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
|
||||
fn actually_delete(model: &RefCell<Model>, siv: &mut Cursive) {
|
||||
let model = &*model.borrow();
|
||||
let new_limits: Vec<_> =
|
||||
model.cameras.iter()
|
||||
.map(|(&id, c)| dir::NewLimit{camera_id: id, limit: c.retain.unwrap()})
|
||||
model.streams.iter()
|
||||
.map(|(&id, s)| dir::NewLimit {stream_id: id, limit: s.retain.unwrap()})
|
||||
.collect();
|
||||
siv.pop_layer(); // deletion confirmation
|
||||
siv.pop_layer(); // retention dialog
|
||||
@@ -150,11 +149,11 @@ fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
|
||||
if model.borrow().errors > 0 {
|
||||
return;
|
||||
}
|
||||
let to_delete = model.borrow().cameras.values().map(
|
||||
|c| ::std::cmp::max(c.used - c.retain.unwrap(), 0)).sum();
|
||||
info!("change press, to_delete={}", to_delete);
|
||||
let to_delete = model.borrow().streams.values().map(
|
||||
|s| ::std::cmp::max(s.used - s.retain.unwrap(), 0)).sum();
|
||||
debug!("change press, to_delete={}", to_delete);
|
||||
if to_delete > 0 {
|
||||
let prompt = format!("Some cameras' usage exceeds new limit. Please confirm the amount \
|
||||
let prompt = format!("Some streams' usage exceeds new limit. Please confirm the amount \
|
||||
of data to delete by typing it back:\n\n{}", encode_size(to_delete));
|
||||
let dialog = views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
@@ -179,19 +178,20 @@ fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
|
||||
|
||||
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
|
||||
let model = {
|
||||
let mut cameras = BTreeMap::new();
|
||||
let mut streams = BTreeMap::new();
|
||||
let mut total_used = 0;
|
||||
let mut total_retain = 0;
|
||||
{
|
||||
let db = db.lock();
|
||||
for (&id, camera) in db.cameras_by_id() {
|
||||
cameras.insert(id, Camera{
|
||||
label: format!("{}: {}", id, camera.short_name),
|
||||
used: camera.sample_file_bytes,
|
||||
retain: Some(camera.retain_bytes),
|
||||
for (&id, s) in db.streams_by_id() {
|
||||
let c = db.cameras_by_id().get(&s.camera_id).expect("stream without camera");
|
||||
streams.insert(id, Stream {
|
||||
label: format!("{}: {}: {}", id, c.short_name, s.type_.as_str()),
|
||||
used: s.sample_file_bytes,
|
||||
retain: Some(s.retain_bytes),
|
||||
});
|
||||
total_used += camera.sample_file_bytes;
|
||||
total_retain += camera.retain_bytes;
|
||||
total_used += s.sample_file_bytes;
|
||||
total_retain += s.retain_bytes;
|
||||
}
|
||||
}
|
||||
let stat = dir.statfs().unwrap();
|
||||
@@ -199,27 +199,27 @@ pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &m
|
||||
Rc::new(RefCell::new(Model{
|
||||
dir: dir.clone(),
|
||||
db: db.clone(),
|
||||
fs_capacity: fs_capacity,
|
||||
total_used: total_used,
|
||||
total_retain: total_retain,
|
||||
fs_capacity,
|
||||
total_used,
|
||||
total_retain,
|
||||
errors: (total_retain > fs_capacity) as isize,
|
||||
cameras: cameras,
|
||||
streams,
|
||||
}))
|
||||
};
|
||||
|
||||
let mut list = views::ListView::new();
|
||||
list.add_child(
|
||||
"camera",
|
||||
"stream",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new("usage").fixed_width(25))
|
||||
.child(views::TextView::new("limit").fixed_width(25)));
|
||||
for (&id, camera) in &model.borrow().cameras {
|
||||
for (&id, stream) in &model.borrow().streams {
|
||||
list.add_child(
|
||||
&camera.label,
|
||||
&stream.label,
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new(encode_size(camera.used)).fixed_width(25))
|
||||
.child(views::TextView::new(encode_size(stream.used)).fixed_width(25))
|
||||
.child(views::EditView::new()
|
||||
.content(encode_size(camera.retain.unwrap()))
|
||||
.content(encode_size(stream.retain.unwrap()))
|
||||
.on_edit({
|
||||
let model = model.clone();
|
||||
move |siv, content, _pos| edit_limit(&model, siv, id, content)
|
||||
|
||||
Reference in New Issue
Block a user