mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-26 22:23:16 -05:00
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:
parent
afe693ef95
commit
a9f64798d6
62
db/db.rs
62
db/db.rs
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
|
@ -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()))
|
||||||
|
@ -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|| {
|
||||||
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user