mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-04-12 23:42:27 -04:00
config: validate more in cameras dialog box
Fixes #152 This won't win any awards for best UI or cleanest UI code, but it's an improvement. Long-term I want the web config UI instead.
This commit is contained in:
parent
30cea5cfcb
commit
95dec9791c
1
server/Cargo.lock
generated
1
server/Cargo.lock
generated
@ -1254,6 +1254,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ smallvec = "1.0"
|
|||||||
tempfile = "3.2.0"
|
tempfile = "3.2.0"
|
||||||
time = "0.1"
|
time = "0.1"
|
||||||
tokio = { version = "1.0", features = ["macros", "parking_lot", "rt-multi-thread", "sync"] }
|
tokio = { version = "1.0", features = ["macros", "parking_lot", "rt-multi-thread", "sync"] }
|
||||||
|
url = "2.1.1"
|
||||||
uuid = { version = "0.8", features = ["std", "v4"] }
|
uuid = { version = "0.8", features = ["std", "v4"] }
|
||||||
itertools = "0.10.0"
|
itertools = "0.10.0"
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ use std::str;
|
|||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Expected schema version. See `guide/schema.md` for more information.
|
/// Expected schema version. See `guide/schema.md` for more information.
|
||||||
@ -499,7 +500,7 @@ pub struct LiveSegment {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct StreamChange {
|
pub struct StreamChange {
|
||||||
pub sample_file_dir_id: Option<i32>,
|
pub sample_file_dir_id: Option<i32>,
|
||||||
pub rtsp_url: String,
|
pub rtsp_url: Option<Url>,
|
||||||
pub record: bool,
|
pub record: bool,
|
||||||
pub flush_if_sec: i64,
|
pub flush_if_sec: i64,
|
||||||
}
|
}
|
||||||
@ -681,7 +682,7 @@ impl StreamStateChanger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !have_data
|
if !have_data
|
||||||
&& sc.rtsp_url.is_empty()
|
&& sc.rtsp_url.is_none()
|
||||||
&& sc.sample_file_dir_id.is_none()
|
&& sc.sample_file_dir_id.is_none()
|
||||||
&& !sc.record
|
&& !sc.record
|
||||||
{
|
{
|
||||||
@ -709,7 +710,7 @@ impl StreamStateChanger {
|
|||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.execute(named_params! {
|
let rows = stmt.execute(named_params! {
|
||||||
":rtsp_url": &sc.rtsp_url,
|
":rtsp_url": &sc.rtsp_url.as_ref().map(Url::as_str),
|
||||||
":record": sc.record,
|
":record": sc.record,
|
||||||
":flush_if_sec": sc.flush_if_sec,
|
":flush_if_sec": sc.flush_if_sec,
|
||||||
":sample_file_dir_id": sc.sample_file_dir_id,
|
":sample_file_dir_id": sc.sample_file_dir_id,
|
||||||
@ -723,7 +724,7 @@ impl StreamStateChanger {
|
|||||||
streams.push((sid, Some((camera_id, type_, sc))));
|
streams.push((sid, Some((camera_id, type_, sc))));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if sc.rtsp_url.is_empty() && sc.sample_file_dir_id.is_none() && !sc.record {
|
if sc.rtsp_url.is_none() && sc.sample_file_dir_id.is_none() && !sc.record {
|
||||||
// Do nothing; there is no record and we want to keep it that way.
|
// Do nothing; there is no record and we want to keep it that way.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -742,7 +743,7 @@ impl StreamStateChanger {
|
|||||||
":camera_id": camera_id,
|
":camera_id": camera_id,
|
||||||
":sample_file_dir_id": sc.sample_file_dir_id,
|
":sample_file_dir_id": sc.sample_file_dir_id,
|
||||||
":type": type_.as_str(),
|
":type": type_.as_str(),
|
||||||
":rtsp_url": &sc.rtsp_url,
|
":rtsp_url": &sc.rtsp_url.as_ref().map(Url::as_str),
|
||||||
":record": sc.record,
|
":record": sc.record,
|
||||||
":flush_if_sec": sc.flush_if_sec,
|
":flush_if_sec": sc.flush_if_sec,
|
||||||
})?;
|
})?;
|
||||||
@ -767,7 +768,7 @@ impl StreamStateChanger {
|
|||||||
type_,
|
type_,
|
||||||
camera_id,
|
camera_id,
|
||||||
sample_file_dir_id: sc.sample_file_dir_id,
|
sample_file_dir_id: sc.sample_file_dir_id,
|
||||||
rtsp_url: mem::replace(&mut sc.rtsp_url, String::new()),
|
rtsp_url: sc.rtsp_url.take().map(String::from).unwrap_or_default(),
|
||||||
retain_bytes: 0,
|
retain_bytes: 0,
|
||||||
flush_if_sec: sc.flush_if_sec,
|
flush_if_sec: sc.flush_if_sec,
|
||||||
range: None,
|
range: None,
|
||||||
@ -790,10 +791,10 @@ impl StreamStateChanger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
(Entry::Vacant(_), None) => {}
|
(Entry::Vacant(_), None) => {}
|
||||||
(Entry::Occupied(e), Some((_, _, sc))) => {
|
(Entry::Occupied(e), Some((_, _, mut sc))) => {
|
||||||
let e = e.into_mut();
|
let e = e.into_mut();
|
||||||
e.sample_file_dir_id = sc.sample_file_dir_id;
|
e.sample_file_dir_id = sc.sample_file_dir_id;
|
||||||
e.rtsp_url = sc.rtsp_url;
|
e.rtsp_url = sc.rtsp_url.take().map(String::from).unwrap_or_default();
|
||||||
e.record = sc.record;
|
e.record = sc.record;
|
||||||
e.flush_if_sec = sc.flush_if_sec;
|
e.flush_if_sec = sc.flush_if_sec;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ impl<C: Clocks + Clone> TestDb<C> {
|
|||||||
streams: [
|
streams: [
|
||||||
db::StreamChange {
|
db::StreamChange {
|
||||||
sample_file_dir_id: Some(sample_file_dir_id),
|
sample_file_dir_id: Some(sample_file_dir_id),
|
||||||
rtsp_url: "rtsp://test-camera/main".to_owned(),
|
rtsp_url: Some(url::Url::parse("rtsp://test-camera/main").unwrap()),
|
||||||
record: true,
|
record: true,
|
||||||
flush_if_sec,
|
flush_if_sec,
|
||||||
},
|
},
|
||||||
|
@ -5,17 +5,17 @@
|
|||||||
use crate::stream::{self, Opener};
|
use crate::stream::{self, Opener};
|
||||||
use base::strutil::{decode_size, encode_size};
|
use base::strutil::{decode_size, encode_size};
|
||||||
use cursive::traits::{Boxable, Finder, Identifiable};
|
use cursive::traits::{Boxable, Finder, Identifiable};
|
||||||
use cursive::views;
|
use cursive::views::{self, ViewRef};
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
use db::writer;
|
use db::writer;
|
||||||
use failure::Error;
|
use failure::{bail, Error, ResultExt};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
|
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
|
||||||
fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
fn get_change(siv: &mut Cursive) -> Result<db::CameraChange, Error> {
|
||||||
// Note: these find_name calls are separate statements, which seems to be important:
|
// Note: these find_name calls are separate statements, which seems to be important:
|
||||||
// https://github.com/gyscos/Cursive/issues/144
|
// https://github.com/gyscos/Cursive/issues/144
|
||||||
let sn = siv
|
let sn = siv
|
||||||
@ -62,49 +62,75 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
|||||||
streams: Default::default(),
|
streams: Default::default(),
|
||||||
};
|
};
|
||||||
for &t in &db::ALL_STREAM_TYPES {
|
for &t in &db::ALL_STREAM_TYPES {
|
||||||
let u = siv
|
let rtsp_url = parse_url(
|
||||||
.find_name::<views::EditView>(&format!("{}_rtsp_url", t.as_str()))
|
siv.find_name::<views::EditView>(&format!("{}_rtsp_url", t.as_str()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_content()
|
.get_content()
|
||||||
.as_str()
|
.as_str(),
|
||||||
.into();
|
)?;
|
||||||
let r = siv
|
let record = siv
|
||||||
.find_name::<views::Checkbox>(&format!("{}_record", t.as_str()))
|
.find_name::<views::Checkbox>(&format!("{}_record", t.as_str()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_checked();
|
.is_checked();
|
||||||
let f = i64::from_str(
|
let flush_if_sec = i64::from_str(
|
||||||
siv.find_name::<views::EditView>(&format!("{}_flush_if_sec", t.as_str()))
|
siv.find_name::<views::EditView>(&format!("{}_flush_if_sec", t.as_str()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_content()
|
.get_content()
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let d = *siv
|
let sample_file_dir_id = *siv
|
||||||
.find_name::<views::SelectView<Option<i32>>>(&format!("{}_sample_file_dir", t.as_str()))
|
.find_name::<views::SelectView<Option<i32>>>(&format!("{}_sample_file_dir", t.as_str()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.selection()
|
.selection()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
c.streams[t.index()] = db::StreamChange {
|
c.streams[t.index()] = db::StreamChange {
|
||||||
rtsp_url: u,
|
rtsp_url,
|
||||||
sample_file_dir_id: d,
|
sample_file_dir_id,
|
||||||
record: r,
|
record,
|
||||||
flush_if_sec: f,
|
flush_if_sec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
c
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse a URL field into a sort-of-validated URL.
|
||||||
|
fn parse_url(raw: &str) -> Result<Option<Url>, Error> {
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let url = url::Url::parse(&raw).with_context(|_| format!("can't parse {:?} as URL", &raw))?;
|
||||||
|
if url.scheme() != "rtsp" {
|
||||||
|
bail!("Expected URL scheme rtsp:// in URL {}", &url);
|
||||||
|
}
|
||||||
|
if !url.username().is_empty() || url.password().is_some() {
|
||||||
|
bail!(
|
||||||
|
"Unexpected credentials in URL {}; use the username and password fields instead",
|
||||||
|
&url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Some(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
|
fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
|
||||||
let change = get_change(siv);
|
let result = (|| {
|
||||||
|
let change = get_change(siv)?;
|
||||||
let result = {
|
for (i, stream) in change.streams.iter().enumerate() {
|
||||||
|
if stream.record && (stream.rtsp_url.is_none() || stream.sample_file_dir_id.is_none()) {
|
||||||
|
let type_ = db::StreamType::from_index(i).unwrap();
|
||||||
|
bail!(
|
||||||
|
"Can't record {} stream without RTSP URL and sample file directory",
|
||||||
|
type_.as_str()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut l = db.lock();
|
let mut l = db.lock();
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
l.update_camera(id, change)
|
l.update_camera(id, change)
|
||||||
} else {
|
} else {
|
||||||
l.add_camera(change).map(|_| ())
|
l.add_camera(change).map(|_| ())
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
siv.add_layer(
|
siv.add_layer(
|
||||||
views::Dialog::text(format!("Unable to add camera: {}", e))
|
views::Dialog::text(format!("Unable to add camera: {}", e))
|
||||||
@ -140,18 +166,21 @@ fn press_test_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn press_test(siv: &mut Cursive, t: db::StreamType) {
|
fn press_test(siv: &mut Cursive, t: db::StreamType) {
|
||||||
let c = get_change(siv);
|
let mut c = match get_change(siv) {
|
||||||
let url = match Url::parse(&c.streams[t.index()].rtsp_url) {
|
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
siv.add_layer(
|
siv.add_layer(
|
||||||
views::Dialog::text(format!("Unparseable URL: {}", e))
|
views::Dialog::text(format!("{}", e))
|
||||||
.title("Stream test failed")
|
.title("Stream test failed")
|
||||||
.dismiss_button("Back"),
|
.dismiss_button("Back"),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let url = c.streams[t.index()]
|
||||||
|
.rtsp_url
|
||||||
|
.take()
|
||||||
|
.expect("test button only enabled when URL set");
|
||||||
let username = c.username;
|
let username = c.username;
|
||||||
let password = c.password;
|
let password = c.password;
|
||||||
|
|
||||||
@ -317,6 +346,11 @@ fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit_url(content: &str, mut test_button: ViewRef<views::Button>) {
|
||||||
|
let enable_test = matches!(parse_url(content), Ok(Some(_)));
|
||||||
|
test_button.set_enabled(enable_test);
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds or updates a camera.
|
/// Adds or updates a camera.
|
||||||
/// (The former if `item` is None; the latter otherwise.)
|
/// (The former if `item` is None; the latter otherwise.)
|
||||||
fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i32>) {
|
fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i32>) {
|
||||||
@ -358,13 +392,21 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
|
|||||||
views::LinearLayout::horizontal()
|
views::LinearLayout::horizontal()
|
||||||
.child(
|
.child(
|
||||||
views::EditView::new()
|
views::EditView::new()
|
||||||
|
.on_edit(move |siv, content, _pos| {
|
||||||
|
let test_button = siv
|
||||||
|
.find_name::<views::Button>(&format!("{}_test", type_.as_str()))
|
||||||
|
.unwrap();
|
||||||
|
edit_url(content, test_button)
|
||||||
|
})
|
||||||
.with_name(format!("{}_rtsp_url", type_.as_str()))
|
.with_name(format!("{}_rtsp_url", type_.as_str()))
|
||||||
.full_width(),
|
.full_width(),
|
||||||
)
|
)
|
||||||
.child(views::DummyView)
|
.child(views::DummyView)
|
||||||
.child(views::Button::new("Test", move |siv| {
|
.child(
|
||||||
press_test(siv, type_)
|
views::Button::new("Test", move |siv| press_test(siv, type_))
|
||||||
})),
|
.disabled()
|
||||||
|
.with_name(format!("{}_test", type_.as_str())),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
"sample file dir",
|
"sample file dir",
|
||||||
@ -431,6 +473,10 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
|
|||||||
&format!("{}_rtsp_url", t.as_str()),
|
&format!("{}_rtsp_url", t.as_str()),
|
||||||
|v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()),
|
|v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()),
|
||||||
);
|
);
|
||||||
|
let test_button = dialog
|
||||||
|
.find_name::<views::Button>(&format!("{}_test", t.as_str()))
|
||||||
|
.unwrap();
|
||||||
|
edit_url(&s.rtsp_url, test_button);
|
||||||
dialog.call_on_name(
|
dialog.call_on_name(
|
||||||
&format!("{}_usage_cap", t.as_str()),
|
&format!("{}_usage_cap", t.as_str()),
|
||||||
|v: &mut views::TextView| v.set_content(u),
|
|v: &mut views::TextView| v.set_content(u),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user