mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-27 04:46:54 -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:
@@ -38,6 +38,7 @@ use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use super::{decode_size, encode_size};
|
||||
use url::Url;
|
||||
|
||||
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
|
||||
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
|
||||
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 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 p = siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into();
|
||||
let mut c = db::CameraChange {
|
||||
short_name: sn,
|
||||
description: d,
|
||||
host: h,
|
||||
onvif_host: h,
|
||||
username: u,
|
||||
password: p,
|
||||
streams: Default::default(),
|
||||
};
|
||||
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();
|
||||
let r = siv.find_id::<views::Checkbox>(&format!("{}_record", t.as_str()))
|
||||
.unwrap().is_checked();
|
||||
@@ -68,7 +69,7 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
||||
&format!("{}_sample_file_dir", t.as_str()))
|
||||
.unwrap().selection().unwrap();
|
||||
c.streams[t.index()] = db::StreamChange {
|
||||
rtsp_path: p,
|
||||
rtsp_url: u,
|
||||
sample_file_dir_id: d,
|
||||
record: r,
|
||||
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 {
|
||||
url,
|
||||
redacted_url: url,
|
||||
url: url.as_str(),
|
||||
redacted_url: url.as_str(), // don't need redaction in config UI.
|
||||
})?;
|
||||
let extra_data = stream.get_extra_data()?;
|
||||
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) {
|
||||
let c = get_change(siv);
|
||||
let url = format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host,
|
||||
c.streams[t.index()].rtsp_path);
|
||||
let mut url = match Url::parse(&c.streams[t.index()].rtsp_url) {
|
||||
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 \
|
||||
on timeout or if you have a long key frame interval",
|
||||
t.as_str(), url))
|
||||
t.as_str(), &url))
|
||||
.title("Testing"));
|
||||
|
||||
// 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 {
|
||||
Err(ref e) => {
|
||||
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")
|
||||
.dismiss_button("Back"));
|
||||
return;
|
||||
@@ -140,7 +154,7 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
|
||||
Ok(ref d) => d,
|
||||
};
|
||||
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")
|
||||
.dismiss_button("Back"));
|
||||
})).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("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("password", views::EditView::new().with_id("password"))
|
||||
.min_height(6);
|
||||
@@ -264,9 +278,9 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
|
||||
.collect();
|
||||
for &type_ in &db::ALL_STREAM_TYPES {
|
||||
let list = views::ListView::new()
|
||||
.child("rtsp path", views::LinearLayout::horizontal()
|
||||
.child("rtsp url", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new()
|
||||
.with_id(format!("{}_rtsp_path", type_.as_str()))
|
||||
.with_id(format!("{}_rtsp_url", type_.as_str()))
|
||||
.full_width())
|
||||
.child(views::DummyView)
|
||||
.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,
|
||||
100. * s.sample_file_bytes as f32 / s.retain_bytes as f32)
|
||||
};
|
||||
dialog.call_on_id(&format!("{}_rtsp_path", t.as_str()),
|
||||
|v: &mut views::EditView| v.set_content(s.rtsp_path.to_owned()));
|
||||
dialog.call_on_id(&format!("{}_rtsp_url", t.as_str()),
|
||||
|v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()));
|
||||
dialog.call_on_id(&format!("{}_usage_cap", t.as_str()),
|
||||
|v: &mut views::TextView| v.set_content(u));
|
||||
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();
|
||||
for &(view_id, content) in &[("short_name", &*camera.short_name),
|
||||
("host", &*camera.host),
|
||||
("onvif_host", &*camera.onvif_host),
|
||||
("username", &*camera.username),
|
||||
("password", &*camera.password)] {
|
||||
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(),
|
||||
syncer.channel.clone(), *id, camera, stream,
|
||||
rotate_offset_sec,
|
||||
streamer::ROTATE_INTERVAL_SEC);
|
||||
streamer::ROTATE_INTERVAL_SEC)?;
|
||||
info!("Starting streamer for {}", streamer.short_name());
|
||||
let name = format!("s-{}", streamer.short_name());
|
||||
streamers.push(thread::Builder::new().name(name).spawn(move|| {
|
||||
|
||||
@@ -93,7 +93,7 @@ pub struct Camera<'a> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct CameraConfig<'a> {
|
||||
pub host: &'a str,
|
||||
pub onvif_host: &'a str,
|
||||
pub username: &'a str,
|
||||
pub password: &'a str,
|
||||
}
|
||||
@@ -184,7 +184,7 @@ impl<'a> Camera<'a> {
|
||||
config: match include_config {
|
||||
false => None,
|
||||
true => Some(CameraConfig {
|
||||
host: &c.host,
|
||||
onvif_host: &c.onvif_host,
|
||||
username: &c.username,
|
||||
password: &c.password,
|
||||
}),
|
||||
|
||||
@@ -38,6 +38,7 @@ use std::result::Result;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use time;
|
||||
use url::Url;
|
||||
|
||||
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>,
|
||||
stream_id: i32,
|
||||
short_name: String,
|
||||
url: String,
|
||||
redacted_url: String,
|
||||
url: Url,
|
||||
redacted_url: Url,
|
||||
}
|
||||
|
||||
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>,
|
||||
syncer_channel: writer::SyncerChannel<::std::fs::File>,
|
||||
stream_id: i32, c: &Camera, s: &Stream, rotate_offset_sec: i64,
|
||||
rotate_interval_sec: i64) -> Self {
|
||||
Streamer {
|
||||
rotate_interval_sec: i64) -> Result<Self, Error> {
|
||||
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(),
|
||||
rotate_offset_sec: rotate_offset_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,
|
||||
stream_id: stream_id,
|
||||
short_name: format!("{}-{}", c.short_name, s.type_.as_str()),
|
||||
url: format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, s.rtsp_path),
|
||||
redacted_url: format!("rtsp://{}:redacted@{}{}", c.username, c.host, s.rtsp_path),
|
||||
}
|
||||
url,
|
||||
redacted_url,
|
||||
})
|
||||
}
|
||||
|
||||
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 _t = TimerGuard::new(&clocks, || format!("opening {}", self.redacted_url));
|
||||
self.opener.open(stream::Source::Rtsp {
|
||||
url: &self.url,
|
||||
redacted_url: &self.redacted_url,
|
||||
url: self.url.as_str(),
|
||||
redacted_url: self.redacted_url.as_str(),
|
||||
})?
|
||||
};
|
||||
let realtime_offset = self.db.clocks().realtime() - clocks.monotonic();
|
||||
|
||||
Reference in New Issue
Block a user