store full rtsp urls

My dad's "GW-GW4089IP" cameras use separate ports for the main and sub
streams:

rtsp://192.168.1.110:5050/H264?channel=0&subtype=0&unicast=true&proto=Onvif
rtsp://192.168.1.110:5049/H264?channel=0&subtype=1&unicast=true&proto=Onvif

Previously I could get one of the streams to work by including :5050 or
:5049 in the host field of the camera. But not both. Now make the
camera's host field reflect the ONVIF port (which is also non-standard
on these cameras, :85). It's not directly used yet but probably will be
sooner or later. Make each stream know its full URL.
This commit is contained in:
Scott Lamb 2019-06-30 23:54:52 -05:00
parent afe693ef95
commit a9f64798d6
9 changed files with 148 additions and 68 deletions

View File

@ -348,7 +348,7 @@ pub struct Camera {
pub uuid: Uuid, pub uuid: Uuid,
pub short_name: String, pub short_name: String,
pub description: String, pub description: String,
pub host: String, pub onvif_host: String,
pub username: String, pub username: String,
pub password: String, pub password: String,
pub streams: [Option<i32>; 2], pub streams: [Option<i32>; 2],
@ -402,7 +402,7 @@ pub struct Stream {
pub camera_id: i32, pub camera_id: i32,
pub sample_file_dir_id: Option<i32>, pub sample_file_dir_id: Option<i32>,
pub type_: StreamType, pub type_: StreamType,
pub rtsp_path: String, pub rtsp_url: String,
pub retain_bytes: i64, pub retain_bytes: i64,
pub flush_if_sec: i64, pub flush_if_sec: i64,
@ -465,7 +465,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_path: String, pub rtsp_url: String,
pub record: bool, pub record: bool,
pub flush_if_sec: i64, pub flush_if_sec: i64,
} }
@ -475,7 +475,7 @@ pub struct StreamChange {
pub struct CameraChange { pub struct CameraChange {
pub short_name: String, pub short_name: String,
pub description: String, pub description: String,
pub host: String, pub onvif_host: String,
pub username: String, pub username: String,
pub password: String, pub password: String,
@ -678,7 +678,7 @@ impl StreamStateChanger {
d, sc.sample_file_dir_id, sid); d, sc.sample_file_dir_id, sid);
} }
} }
if !have_data && sc.rtsp_path.is_empty() && sc.sample_file_dir_id.is_none() && if !have_data && sc.rtsp_url.is_empty() && sc.sample_file_dir_id.is_none() &&
!sc.record { !sc.record {
// Delete stream. // Delete stream.
let mut stmt = tx.prepare_cached(r#" let mut stmt = tx.prepare_cached(r#"
@ -692,7 +692,7 @@ impl StreamStateChanger {
// Update stream. // Update stream.
let mut stmt = tx.prepare_cached(r#" let mut stmt = tx.prepare_cached(r#"
update stream set update stream set
rtsp_path = :rtsp_path, rtsp_url = :rtsp_url,
record = :record, record = :record,
flush_if_sec = :flush_if_sec, flush_if_sec = :flush_if_sec,
sample_file_dir_id = :sample_file_dir_id sample_file_dir_id = :sample_file_dir_id
@ -700,7 +700,7 @@ impl StreamStateChanger {
id = :id id = :id
"#)?; "#)?;
let rows = stmt.execute_named(&[ let rows = stmt.execute_named(&[
(":rtsp_path", &sc.rtsp_path), (":rtsp_url", &sc.rtsp_url),
(":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),
@ -714,22 +714,22 @@ impl StreamStateChanger {
streams.push((sid, Some((camera_id, type_, sc)))); streams.push((sid, Some((camera_id, type_, sc))));
} }
} else { } else {
if sc.rtsp_path.is_empty() && sc.sample_file_dir_id.is_none() && !sc.record { if sc.rtsp_url.is_empty() && 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;
} }
// Insert stream. // Insert stream.
let mut stmt = tx.prepare_cached(r#" let mut stmt = tx.prepare_cached(r#"
insert into stream (camera_id, sample_file_dir_id, type, rtsp_path, record, insert into stream (camera_id, sample_file_dir_id, type, rtsp_url, record,
retain_bytes, flush_if_sec, next_recording_id) retain_bytes, flush_if_sec, next_recording_id)
values (:camera_id, :sample_file_dir_id, :type, :rtsp_path, :record, values (:camera_id, :sample_file_dir_id, :type, :rtsp_url, :record,
0, :flush_if_sec, 1) 0, :flush_if_sec, 1)
"#)?; "#)?;
stmt.execute_named(&[ stmt.execute_named(&[
(":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_path", &sc.rtsp_path), (":rtsp_url", &sc.rtsp_url),
(":record", &sc.record), (":record", &sc.record),
(":flush_if_sec", &sc.flush_if_sec), (":flush_if_sec", &sc.flush_if_sec),
])?; ])?;
@ -757,7 +757,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_path: mem::replace(&mut sc.rtsp_path, String::new()), rtsp_url: mem::replace(&mut sc.rtsp_url, String::new()),
retain_bytes: 0, retain_bytes: 0,
flush_if_sec: sc.flush_if_sec, flush_if_sec: sc.flush_if_sec,
range: None, range: None,
@ -778,7 +778,7 @@ impl StreamStateChanger {
(Entry::Occupied(e), Some((_, _, sc))) => { (Entry::Occupied(e), Some((_, _, 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_path = sc.rtsp_path; e.rtsp_url = sc.rtsp_url;
e.record = sc.record; e.record = sc.record;
e.flush_if_sec = sc.flush_if_sec; e.flush_if_sec = sc.flush_if_sec;
}, },
@ -1401,7 +1401,7 @@ impl LockedDatabase {
uuid, uuid,
short_name, short_name,
description, description,
host, onvif_host,
username, username,
password password
from from
@ -1416,7 +1416,7 @@ impl LockedDatabase {
uuid: uuid.0, uuid: uuid.0,
short_name: row.get(2)?, short_name: row.get(2)?,
description: row.get(3)?, description: row.get(3)?,
host: row.get(4)?, onvif_host: row.get(4)?,
username: row.get(5)?, username: row.get(5)?,
password: row.get(6)?, password: row.get(6)?,
streams: Default::default(), streams: Default::default(),
@ -1437,7 +1437,7 @@ impl LockedDatabase {
type, type,
camera_id, camera_id,
sample_file_dir_id, sample_file_dir_id,
rtsp_path, rtsp_url,
retain_bytes, retain_bytes,
flush_if_sec, flush_if_sec,
next_recording_id, next_recording_id,
@ -1463,7 +1463,7 @@ impl LockedDatabase {
type_, type_,
camera_id, camera_id,
sample_file_dir_id: row.get(3)?, sample_file_dir_id: row.get(3)?,
rtsp_path: row.get(4)?, rtsp_url: row.get(4)?,
retain_bytes: row.get(5)?, retain_bytes: row.get(5)?,
flush_if_sec, flush_if_sec,
range: None, range: None,
@ -1618,14 +1618,16 @@ impl LockedDatabase {
let camera_id; let camera_id;
{ {
let mut stmt = tx.prepare_cached(r#" let mut stmt = tx.prepare_cached(r#"
insert into camera (uuid, short_name, description, host, username, password) insert into camera (uuid, short_name, description, onvif_host, username,
values (:uuid, :short_name, :description, :host, :username, :password) password)
values (:uuid, :short_name, :description, :onvif_host, :username,
:password)
"#)?; "#)?;
stmt.execute_named(&[ stmt.execute_named(&[
(":uuid", &uuid_bytes), (":uuid", &uuid_bytes),
(":short_name", &camera.short_name), (":short_name", &camera.short_name),
(":description", &camera.description), (":description", &camera.description),
(":host", &camera.host), (":onvif_host", &camera.onvif_host),
(":username", &camera.username), (":username", &camera.username),
(":password", &camera.password), (":password", &camera.password),
])?; ])?;
@ -1640,7 +1642,7 @@ impl LockedDatabase {
uuid, uuid,
short_name: camera.short_name, short_name: camera.short_name,
description: camera.description, description: camera.description,
host: camera.host, onvif_host: camera.onvif_host,
username: camera.username, username: camera.username,
password: camera.password, password: camera.password,
streams, streams,
@ -1664,7 +1666,7 @@ impl LockedDatabase {
update camera set update camera set
short_name = :short_name, short_name = :short_name,
description = :description, description = :description,
host = :host, onvif_host = :onvif_host,
username = :username, username = :username,
password = :password password = :password
where where
@ -1674,7 +1676,7 @@ impl LockedDatabase {
(":id", &camera_id), (":id", &camera_id),
(":short_name", &camera.short_name), (":short_name", &camera.short_name),
(":description", &camera.description), (":description", &camera.description),
(":host", &camera.host), (":onvif_host", &camera.onvif_host),
(":username", &camera.username), (":username", &camera.username),
(":password", &camera.password), (":password", &camera.password),
])?; ])?;
@ -1685,7 +1687,7 @@ impl LockedDatabase {
tx.commit()?; tx.commit()?;
c.short_name = camera.short_name; c.short_name = camera.short_name;
c.description = camera.description; c.description = camera.description;
c.host = camera.host; c.onvif_host = camera.onvif_host;
c.username = camera.username; c.username = camera.username;
c.password = camera.password; c.password = camera.password;
c.streams = streams.apply(&mut self.streams_by_id); c.streams = streams.apply(&mut self.streams_by_id);
@ -2036,11 +2038,11 @@ mod tests {
rows += 1; rows += 1;
camera_id = row.id; camera_id = row.id;
assert_eq!(uuid, row.uuid); assert_eq!(uuid, row.uuid);
assert_eq!("test-camera", row.host); assert_eq!("test-camera", row.onvif_host);
assert_eq!("foo", row.username); assert_eq!("foo", row.username);
assert_eq!("bar", row.password); assert_eq!("bar", row.password);
//assert_eq!("/main", row.main_rtsp_path); //assert_eq!("/main", row.main_rtsp_url);
//assert_eq!("/sub", row.sub_rtsp_path); //assert_eq!("/sub", row.sub_rtsp_url);
//assert_eq!(42, row.retain_bytes); //assert_eq!(42, row.retain_bytes);
//assert_eq!(None, row.range); //assert_eq!(None, row.range);
//assert_eq!(recording::Duration(0), row.duration); //assert_eq!(recording::Duration(0), row.duration);
@ -2225,19 +2227,19 @@ mod tests {
let mut c = CameraChange { let mut c = CameraChange {
short_name: "testcam".to_owned(), short_name: "testcam".to_owned(),
description: "".to_owned(), description: "".to_owned(),
host: "test-camera".to_owned(), onvif_host: "test-camera".to_owned(),
username: "foo".to_owned(), username: "foo".to_owned(),
password: "bar".to_owned(), password: "bar".to_owned(),
streams: [ streams: [
StreamChange { StreamChange {
sample_file_dir_id: Some(sample_file_dir_id), sample_file_dir_id: Some(sample_file_dir_id),
rtsp_path: "/main".to_owned(), rtsp_url: "rtsp://test-camera/main".to_owned(),
record: false, record: false,
flush_if_sec: 1, flush_if_sec: 1,
}, },
StreamChange { StreamChange {
sample_file_dir_id: Some(sample_file_dir_id), sample_file_dir_id: Some(sample_file_dir_id),
rtsp_path: "/sub".to_owned(), rtsp_url: "rtsp://test-camera/sub".to_owned(),
record: true, record: true,
flush_if_sec: 1, flush_if_sec: 1,
}, },

View File

@ -94,8 +94,11 @@ create table camera (
-- A short description of the camera. -- A short description of the camera.
description text, description text,
-- The host (or IP address) to use in rtsp:// URLs when accessing the camera. -- The host part of the http:// URL when accessing ONVIF, optionally
host text, -- including ":<port>". Eg with ONVIF host "192.168.1.110:85", the full URL
-- of the devie management service will be
-- "http://192.168.1.110:85/device_service".
onvif_host text,
-- The username to use when accessing the camera. -- The username to use when accessing the camera.
-- If empty, no username or password will be supplied. -- If empty, no username or password will be supplied.
@ -116,8 +119,9 @@ create table stream (
-- will not be deleted. -- will not be deleted.
record integer not null check (record in (1, 0)), record integer not null check (record in (1, 0)),
-- The path (starting with "/") to use in rtsp:// URLs to for this stream. -- The rtsp:// URL to use for this stream, excluding username and password.
rtsp_path text not null, -- (Those are taken from the camera row's respective fields.)
rtsp_url text not null,
-- The number of bytes of video to retain, excluding the currently-recording -- The number of bytes of video to retain, excluding the currently-recording
-- file. Older files will be deleted as necessary to stay within this limit. -- file. Older files will be deleted as necessary to stay within this limit.

View File

@ -96,13 +96,13 @@ impl<C: Clocks + Clone> TestDb<C> {
assert_eq!(TEST_CAMERA_ID, l.add_camera(db::CameraChange { assert_eq!(TEST_CAMERA_ID, l.add_camera(db::CameraChange {
short_name: "test camera".to_owned(), short_name: "test camera".to_owned(),
description: "".to_owned(), description: "".to_owned(),
host: "test-camera".to_owned(), onvif_host: "test-camera".to_owned(),
username: "foo".to_owned(), username: "foo".to_owned(),
password: "bar".to_owned(), password: "bar".to_owned(),
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_path: "/main".to_owned(), rtsp_url: "rtsp://test-camera/main".to_owned(),
record: true, record: true,
flush_if_sec, flush_if_sec,
}, },

View File

@ -70,6 +70,57 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
-- behavior. Newly created users won't have prepopulated permissions like this. -- behavior. Newly created users won't have prepopulated permissions like this.
update user set permissions = X'0801'; update user set permissions = X'0801';
update user_session set permissions = X'0801'; update user_session set permissions = X'0801';
alter table stream rename to old_stream;
create table stream (
id integer primary key,
camera_id integer not null references camera (id),
sample_file_dir_id integer references sample_file_dir (id),
type text not null check (type in ('main', 'sub')),
record integer not null check (record in (1, 0)),
rtsp_url text not null,
retain_bytes integer not null check (retain_bytes >= 0),
flush_if_sec integer not null,
next_recording_id integer not null check (next_recording_id >= 0),
unique (camera_id, type)
);
insert into stream
select
s.id,
s.camera_id,
s.sample_file_dir_id,
s.type,
s.record,
'rtsp://' || c.host || s.rtsp_path as rtsp_url,
retain_bytes,
flush_if_sec,
next_recording_id
from
old_stream s join camera c on (s.camera_id = c.id);
drop table old_stream;
alter table camera rename to old_camera;
create table camera (
id integer primary key,
uuid blob unique not null check (length(uuid) = 16),
short_name text not null,
description text,
onvif_host text,
username text,
password text
);
insert into camera
select
id,
uuid,
short_name,
description,
host,
username,
password
from
old_camera;
drop table old_camera;
"#)?; "#)?;
Ok(()) Ok(())
} }

View File

@ -76,7 +76,7 @@ The `application/json` response will have a dict as follows:
a dictionary describing the configuration of the camera: a dictionary describing the configuration of the camera:
* `username` * `username`
* `password` * `password`
* `host` * `onvif_host`
* `streams`: a dict of stream type ("main" or "sub") to a dictionary * `streams`: a dict of stream type ("main" or "sub") to a dictionary
describing the stream: describing the stream:
* `retainBytes`: the configured total number of bytes of completed * `retainBytes`: the configured total number of bytes of completed
@ -136,7 +136,7 @@ Example response:
"shortName": "driveway", "shortName": "driveway",
"description": "Hikvision DS-2CD2032 overlooking the driveway from east", "description": "Hikvision DS-2CD2032 overlooking the driveway from east",
"config": { "config": {
"host": "192.168.1.100", "onvif_host": "192.168.1.100",
"user": "admin", "user": "admin",
"password": "12345", "password": "12345",
}, },

View File

@ -38,6 +38,7 @@ use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use super::{decode_size, encode_size}; use super::{decode_size, encode_size};
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) -> db::CameraChange {
@ -45,19 +46,19 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
// https://github.com/gyscos/Cursive/issues/144 // https://github.com/gyscos/Cursive/issues/144
let sn = siv.find_id::<views::EditView>("short_name").unwrap().get_content().as_str().into(); let sn = siv.find_id::<views::EditView>("short_name").unwrap().get_content().as_str().into();
let d = siv.find_id::<views::TextArea>("description").unwrap().get_content().into(); let d = siv.find_id::<views::TextArea>("description").unwrap().get_content().into();
let h = siv.find_id::<views::EditView>("host").unwrap().get_content().as_str().into(); let h = siv.find_id::<views::EditView>("onvif_host").unwrap().get_content().as_str().into();
let u = siv.find_id::<views::EditView>("username").unwrap().get_content().as_str().into(); let u = siv.find_id::<views::EditView>("username").unwrap().get_content().as_str().into();
let p = siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into(); let p = siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into();
let mut c = db::CameraChange { let mut c = db::CameraChange {
short_name: sn, short_name: sn,
description: d, description: d,
host: h, onvif_host: h,
username: u, username: u,
password: p, password: p,
streams: Default::default(), streams: Default::default(),
}; };
for &t in &db::ALL_STREAM_TYPES { for &t in &db::ALL_STREAM_TYPES {
let p = siv.find_id::<views::EditView>(&format!("{}_rtsp_path", t.as_str())) let u = siv.find_id::<views::EditView>(&format!("{}_rtsp_url", t.as_str()))
.unwrap().get_content().as_str().into(); .unwrap().get_content().as_str().into();
let r = siv.find_id::<views::Checkbox>(&format!("{}_record", t.as_str())) let r = siv.find_id::<views::Checkbox>(&format!("{}_record", t.as_str()))
.unwrap().is_checked(); .unwrap().is_checked();
@ -68,7 +69,7 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
&format!("{}_sample_file_dir", t.as_str())) &format!("{}_sample_file_dir", t.as_str()))
.unwrap().selection().unwrap(); .unwrap().selection().unwrap();
c.streams[t.index()] = db::StreamChange { c.streams[t.index()] = db::StreamChange {
rtsp_path: p, rtsp_url: u,
sample_file_dir_id: d, sample_file_dir_id: d,
record: r, record: r,
flush_if_sec: f, flush_if_sec: f,
@ -101,10 +102,10 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
} }
} }
fn press_test_inner(url: &str) -> Result<String, Error> { fn press_test_inner(url: &Url) -> Result<String, Error> {
let stream = stream::FFMPEG.open(stream::Source::Rtsp { let stream = stream::FFMPEG.open(stream::Source::Rtsp {
url, url: url.as_str(),
redacted_url: url, redacted_url: url.as_str(), // don't need redaction in config UI.
})?; })?;
let extra_data = stream.get_extra_data()?; let extra_data = stream.get_extra_data()?;
Ok(format!("{}x{} video stream", extra_data.width, extra_data.height)) Ok(format!("{}x{} video stream", extra_data.width, extra_data.height))
@ -112,11 +113,24 @@ fn press_test_inner(url: &str) -> Result<String, Error> {
fn press_test(siv: &mut Cursive, t: db::StreamType) { fn press_test(siv: &mut Cursive, t: db::StreamType) {
let c = get_change(siv); let c = get_change(siv);
let url = format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, let mut url = match Url::parse(&c.streams[t.index()].rtsp_url) {
c.streams[t.index()].rtsp_path); Ok(u) => u,
Err(e) => {
siv.add_layer(views::Dialog::text(
format!("Unparseable URL: {}", e))
.title("Stream test failed")
.dismiss_button("Back"));
return;
},
};
if !c.username.is_empty() {
url.set_username(&c.username);
url.set_password(Some(&c.password));
}
siv.add_layer(views::Dialog::text(format!("Testing {} stream at {}. This may take a while \ siv.add_layer(views::Dialog::text(format!("Testing {} stream at {}. This may take a while \
on timeout or if you have a long key frame interval", on timeout or if you have a long key frame interval",
t.as_str(), url)) t.as_str(), &url))
.title("Testing")); .title("Testing"));
// Let siv have this thread for its event loop; do the work in a background thread. // Let siv have this thread for its event loop; do the work in a background thread.
@ -132,7 +146,7 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
let description = match r { let description = match r {
Err(ref e) => { Err(ref e) => {
siv.add_layer( siv.add_layer(
views::Dialog::text(format!("{} stream at {}:\n\n{}", t.as_str(), url, e)) views::Dialog::text(format!("{} stream at {}:\n\n{}", t.as_str(), &url, e))
.title("Stream test failed") .title("Stream test failed")
.dismiss_button("Back")); .dismiss_button("Back"));
return; return;
@ -140,7 +154,7 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
Ok(ref d) => d, Ok(ref d) => d,
}; };
siv.add_layer(views::Dialog::text( siv.add_layer(views::Dialog::text(
format!("{} stream at {}:\n\n{}", t.as_str(), url, description)) format!("{} stream at {}:\n\n{}", t.as_str(), &url, description))
.title("Stream test succeeded") .title("Stream test succeeded")
.dismiss_button("Back")); .dismiss_button("Back"));
})).unwrap(); })).unwrap();
@ -247,7 +261,7 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
})) }))
.child("uuid", views::TextView::new("<new>").with_id("uuid")) .child("uuid", views::TextView::new("<new>").with_id("uuid"))
.child("short name", views::EditView::new().with_id("short_name")) .child("short name", views::EditView::new().with_id("short_name"))
.child("host", views::EditView::new().with_id("host")) .child("onvif_host", views::EditView::new().with_id("onvif_host"))
.child("username", views::EditView::new().with_id("username")) .child("username", views::EditView::new().with_id("username"))
.child("password", views::EditView::new().with_id("password")) .child("password", views::EditView::new().with_id("password"))
.min_height(6); .min_height(6);
@ -264,9 +278,9 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
.collect(); .collect();
for &type_ in &db::ALL_STREAM_TYPES { for &type_ in &db::ALL_STREAM_TYPES {
let list = views::ListView::new() let list = views::ListView::new()
.child("rtsp path", views::LinearLayout::horizontal() .child("rtsp url", views::LinearLayout::horizontal()
.child(views::EditView::new() .child(views::EditView::new()
.with_id(format!("{}_rtsp_path", type_.as_str())) .with_id(format!("{}_rtsp_url", type_.as_str()))
.full_width()) .full_width())
.child(views::DummyView) .child(views::DummyView)
.child(views::Button::new("Test", move |siv| press_test(siv, type_)))) .child(views::Button::new("Test", move |siv| press_test(siv, type_))))
@ -315,8 +329,8 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
format!("{} / {} ({:.1}%)", s.sample_file_bytes, s.retain_bytes, format!("{} / {} ({:.1}%)", s.sample_file_bytes, s.retain_bytes,
100. * s.sample_file_bytes as f32 / s.retain_bytes as f32) 100. * s.sample_file_bytes as f32 / s.retain_bytes as f32)
}; };
dialog.call_on_id(&format!("{}_rtsp_path", t.as_str()), dialog.call_on_id(&format!("{}_rtsp_url", t.as_str()),
|v: &mut views::EditView| v.set_content(s.rtsp_path.to_owned())); |v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()));
dialog.call_on_id(&format!("{}_usage_cap", t.as_str()), dialog.call_on_id(&format!("{}_usage_cap", t.as_str()),
|v: &mut views::TextView| v.set_content(u)); |v: &mut views::TextView| v.set_content(u));
dialog.call_on_id(&format!("{}_record", t.as_str()), dialog.call_on_id(&format!("{}_record", t.as_str()),
@ -331,7 +345,7 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
} }
let name = camera.short_name.clone(); let name = camera.short_name.clone();
for &(view_id, content) in &[("short_name", &*camera.short_name), for &(view_id, content) in &[("short_name", &*camera.short_name),
("host", &*camera.host), ("onvif_host", &*camera.onvif_host),
("username", &*camera.username), ("username", &*camera.username),
("password", &*camera.password)] { ("password", &*camera.password)] {
dialog.call_on_id(view_id, |v: &mut views::EditView| v.set_content(content.to_string())) dialog.call_on_id(view_id, |v: &mut views::EditView| v.set_content(content.to_string()))

View File

@ -261,7 +261,7 @@ pub fn run() -> Result<(), Error> {
let mut streamer = streamer::Streamer::new(&env, syncer.dir.clone(), let mut streamer = streamer::Streamer::new(&env, syncer.dir.clone(),
syncer.channel.clone(), *id, camera, stream, syncer.channel.clone(), *id, camera, stream,
rotate_offset_sec, rotate_offset_sec,
streamer::ROTATE_INTERVAL_SEC); streamer::ROTATE_INTERVAL_SEC)?;
info!("Starting streamer for {}", streamer.short_name()); info!("Starting streamer for {}", streamer.short_name());
let name = format!("s-{}", streamer.short_name()); let name = format!("s-{}", streamer.short_name());
streamers.push(thread::Builder::new().name(name).spawn(move|| { streamers.push(thread::Builder::new().name(name).spawn(move|| {

View File

@ -93,7 +93,7 @@ pub struct Camera<'a> {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")] #[serde(rename_all="camelCase")]
pub struct CameraConfig<'a> { pub struct CameraConfig<'a> {
pub host: &'a str, pub onvif_host: &'a str,
pub username: &'a str, pub username: &'a str,
pub password: &'a str, pub password: &'a str,
} }
@ -184,7 +184,7 @@ impl<'a> Camera<'a> {
config: match include_config { config: match include_config {
false => None, false => None,
true => Some(CameraConfig { true => Some(CameraConfig {
host: &c.host, onvif_host: &c.onvif_host,
username: &c.username, username: &c.username,
password: &c.password, password: &c.password,
}), }),

View File

@ -38,6 +38,7 @@ use std::result::Result;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use time; use time;
use url::Url;
pub static ROTATE_INTERVAL_SEC: i64 = 60; pub static ROTATE_INTERVAL_SEC: i64 = 60;
@ -60,16 +61,24 @@ pub struct Streamer<'a, C, S> where C: Clocks + Clone, S: 'a + stream::Stream {
opener: &'a dyn stream::Opener<S>, opener: &'a dyn stream::Opener<S>,
stream_id: i32, stream_id: i32,
short_name: String, short_name: String,
url: String, url: Url,
redacted_url: String, redacted_url: Url,
} }
impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::Stream { impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::Stream {
pub fn new<'b>(env: &Environment<'a, 'b, C, S>, dir: Arc<dir::SampleFileDir>, pub fn new<'b>(env: &Environment<'a, 'b, C, S>, dir: Arc<dir::SampleFileDir>,
syncer_channel: writer::SyncerChannel<::std::fs::File>, syncer_channel: writer::SyncerChannel<::std::fs::File>,
stream_id: i32, c: &Camera, s: &Stream, rotate_offset_sec: i64, stream_id: i32, c: &Camera, s: &Stream, rotate_offset_sec: i64,
rotate_interval_sec: i64) -> Self { rotate_interval_sec: i64) -> Result<Self, Error> {
Streamer { let mut url = Url::parse(&s.rtsp_url)?;
let mut redacted_url = url.clone();
if !c.username.is_empty() {
url.set_username(&c.username);
redacted_url.set_username(&c.username);
url.set_password(Some(&c.password));
redacted_url.set_password(Some("redacted"));
}
Ok(Streamer {
shutdown: env.shutdown.clone(), shutdown: env.shutdown.clone(),
rotate_offset_sec: rotate_offset_sec, rotate_offset_sec: rotate_offset_sec,
rotate_interval_sec: rotate_interval_sec, rotate_interval_sec: rotate_interval_sec,
@ -79,9 +88,9 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
opener: env.opener, opener: env.opener,
stream_id: stream_id, stream_id: stream_id,
short_name: format!("{}-{}", c.short_name, s.type_.as_str()), short_name: format!("{}-{}", c.short_name, s.type_.as_str()),
url: format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, s.rtsp_path), url,
redacted_url: format!("rtsp://{}:redacted@{}{}", c.username, c.host, s.rtsp_path), redacted_url,
} })
} }
pub fn short_name(&self) -> &str { &self.short_name } pub fn short_name(&self) -> &str { &self.short_name }
@ -104,8 +113,8 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
let mut stream = { let mut stream = {
let _t = TimerGuard::new(&clocks, || format!("opening {}", self.redacted_url)); let _t = TimerGuard::new(&clocks, || format!("opening {}", self.redacted_url));
self.opener.open(stream::Source::Rtsp { self.opener.open(stream::Source::Rtsp {
url: &self.url, url: self.url.as_str(),
redacted_url: &self.redacted_url, redacted_url: self.redacted_url.as_str(),
})? })?
}; };
let realtime_offset = self.db.clocks().realtime() - clocks.monotonic(); let realtime_offset = self.db.clocks().realtime() - clocks.monotonic();