allow setting rtsp transport per-stream

This commit is contained in:
Scott Lamb 2021-10-27 14:27:23 -07:00
parent 981cee0706
commit 1c9a55653d
5 changed files with 64 additions and 8 deletions

View File

@ -10,6 +10,7 @@ Each release is tagged in Git and on the Docker repository
* bugfix: editing a camera from `nvr config` would erroneously clear the * bugfix: editing a camera from `nvr config` would erroneously clear the
sample file directory associated with its streams. sample file directory associated with its streams.
* RTSP transport (TCP or UDP) can be set per-stream from `nvr config`.
## `v0.7.0` (2021-10-27) ## `v0.7.0` (2021-10-27)

View File

@ -209,6 +209,10 @@ pub struct StreamConfig {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<Url>, pub url: Option<Url>,
/// The RTSP transport (`tcp` or `udp`) to use.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub rtsp_transport: String,
/// The number of bytes of video to retain, excluding the /// The number of bytes of video to retain, excluding the
/// currently-recording file. /// currently-recording file.
/// ///

View File

@ -13,6 +13,7 @@ use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use url::Url; use url::Url;
#[derive(Debug)]
struct Camera { struct Camera {
short_name: String, short_name: String,
description: String, description: String,
@ -22,11 +23,12 @@ struct Camera {
streams: [Stream; db::NUM_STREAM_TYPES], streams: [Stream; db::NUM_STREAM_TYPES],
} }
#[derive(Default)] #[derive(Debug, Default)]
struct Stream { struct Stream {
url: String, url: String,
record: bool, record: bool,
flush_if_sec: String, flush_if_sec: String,
rtsp_transport: &'static str,
sample_file_dir_id: Option<i32>, sample_file_dir_id: Option<i32>,
} }
@ -82,6 +84,11 @@ fn get_camera(siv: &mut Cursive) -> Camera {
.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 rtsp_transport = *siv
.find_name::<views::SelectView<&'static str>>(&format!("{}_rtsp_transport", t.as_str()))
.unwrap()
.selection()
.unwrap();
let flush_if_sec = siv let flush_if_sec = siv
.find_name::<views::EditView>(&format!("{}_flush_if_sec", t.as_str())) .find_name::<views::EditView>(&format!("{}_flush_if_sec", t.as_str()))
.unwrap() .unwrap()
@ -97,9 +104,11 @@ fn get_camera(siv: &mut Cursive) -> Camera {
url, url,
record, record,
flush_if_sec, flush_if_sec,
rtsp_transport,
sample_file_dir_id, sample_file_dir_id,
}; };
} }
log::trace!("camera is: {:#?}", &camera);
camera camera
} }
@ -155,6 +164,7 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
}) })
.to_owned(); .to_owned();
stream_change.config.url = parse_url(&stream.url, &["rtsp"])?; stream_change.config.url = parse_url(&stream.url, &["rtsp"])?;
stream_change.config.rtsp_transport = stream.rtsp_transport.to_owned();
stream_change.sample_file_dir_id = stream.sample_file_dir_id; stream_change.sample_file_dir_id = stream.sample_file_dir_id;
stream_change.config.flush_if_sec = if stream.flush_if_sec.is_empty() { stream_change.config.flush_if_sec = if stream.flush_if_sec.is_empty() {
0 0
@ -175,9 +185,13 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
})(); })();
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!(
.title("Error") "Unable to {} camera: {}",
.dismiss_button("Abort"), if id.is_some() { "edit" } else { "add" },
e
))
.title("Error")
.dismiss_button("Abort"),
); );
} else { } else {
siv.pop_layer(); // get rid of the add/edit camera dialog. siv.pop_layer(); // get rid of the add/edit camera dialog.
@ -457,6 +471,13 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
"record", "record",
views::Checkbox::new().with_name(format!("{}_record", type_.as_str())), views::Checkbox::new().with_name(format!("{}_record", type_.as_str())),
) )
.child(
"rtsp_transport",
views::SelectView::<&str>::new()
.with_all([("(default)", ""), ("tcp", "tcp"), ("udp", "udp")])
.popup()
.with_name(format!("{}_rtsp_transport", type_.as_str())),
)
.child( .child(
"flush_if_sec", "flush_if_sec",
views::EditView::new().with_name(format!("{}_flush_if_sec", type_.as_str())), views::EditView::new().with_name(format!("{}_flush_if_sec", type_.as_str())),
@ -529,11 +550,22 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
v.set_checked(s.config.mode == db::json::STREAM_MODE_RECORD) v.set_checked(s.config.mode == db::json::STREAM_MODE_RECORD)
}, },
); );
dialog.call_on_name(
&format!("{}_rtsp_transport", t.as_str()),
|v: &mut views::SelectView<&'static str>| {
v.set_selection(match s.config.rtsp_transport.as_str() {
"tcp" => 1,
"udp" => 2,
_ => 0,
})
},
);
dialog.call_on_name( dialog.call_on_name(
&format!("{}_flush_if_sec", t.as_str()), &format!("{}_flush_if_sec", t.as_str()),
|v: &mut views::EditView| v.set_content(s.config.flush_if_sec.to_string()), |v: &mut views::EditView| v.set_content(s.config.flush_if_sec.to_string()),
); );
} }
log::debug!("setting {} dir to {}", t.as_str(), selected_dir);
dialog.call_on_name( dialog.call_on_name(
&format!("{}_sample_file_dir", t.as_str()), &format!("{}_sample_file_dir", t.as_str()),
|v: &mut views::SelectView<Option<i32>>| v.set_selection(selected_dir), |v: &mut views::SelectView<Option<i32>>| v.set_selection(selected_dir),

View File

@ -76,6 +76,8 @@ pub struct Args {
#[structopt(long, default_value = "retina", parse(try_from_str))] #[structopt(long, default_value = "retina", parse(try_from_str))]
rtsp_library: crate::stream::RtspLibrary, rtsp_library: crate::stream::RtspLibrary,
/// The RTSP transport (`tcp` or `udp`) to use when none is specified in the
/// per-stream configuration.
#[structopt(long, default_value)] #[structopt(long, default_value)]
rtsp_transport: retina::client::Transport, rtsp_transport: retina::client::Transport,
} }
@ -265,7 +267,7 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32,
let env = streamer::Environment { let env = streamer::Environment {
db: &db, db: &db,
opener: args.rtsp_library.opener(), opener: args.rtsp_library.opener(),
transport: args.rtsp_transport, default_transport: args.rtsp_transport,
shutdown_rx: &shutdown_rx, shutdown_rx: &shutdown_rx,
}; };

View File

@ -8,6 +8,7 @@ use db::{dir, recording, writer, Camera, Database, Stream};
use failure::{bail, format_err, Error}; use failure::{bail, format_err, Error};
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use std::result::Result; use std::result::Result;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use url::Url; use url::Url;
@ -19,7 +20,7 @@ where
C: Clocks + Clone, C: Clocks + Clone,
{ {
pub opener: &'a dyn stream::Opener, pub opener: &'a dyn stream::Opener,
pub transport: retina::client::Transport, pub default_transport: retina::client::Transport,
pub db: &'tmp Arc<Database<C>>, pub db: &'tmp Arc<Database<C>>,
pub shutdown_rx: &'tmp base::shutdown::Receiver, pub shutdown_rx: &'tmp base::shutdown::Receiver,
} }
@ -71,6 +72,22 @@ where
if !url.username().is_empty() || url.password().is_some() { if !url.username().is_empty() || url.password().is_some() {
bail!("RTSP URL shouldn't include credentials"); bail!("RTSP URL shouldn't include credentials");
} }
let stream_transport = if s.config.rtsp_transport.is_empty() {
None
} else {
match retina::client::Transport::from_str(&s.config.rtsp_transport) {
Ok(t) => Some(t),
Err(_) => {
log::warn!(
"Unable to parse configured transport {:?} for {}/{}; ignoring.",
&s.config.rtsp_transport,
&c.short_name,
s.type_
);
None
}
}
};
Ok(Streamer { Ok(Streamer {
shutdown_rx: env.shutdown_rx.clone(), shutdown_rx: env.shutdown_rx.clone(),
rotate_offset_sec, rotate_offset_sec,
@ -79,7 +96,7 @@ where
dir, dir,
syncer_channel, syncer_channel,
opener: env.opener, opener: env.opener,
transport: env.transport, transport: stream_transport.unwrap_or(env.default_transport),
stream_id, stream_id,
session_group, session_group,
short_name: format!("{}-{}", c.short_name, s.type_.as_str()), short_name: format!("{}-{}", c.short_name, s.type_.as_str()),
@ -417,7 +434,7 @@ mod tests {
opener: &opener, opener: &opener,
db: &db.db, db: &db.db,
shutdown_rx: &shutdown_rx, shutdown_rx: &shutdown_rx,
transport: retina::client::Transport::Tcp, default_transport: retina::client::Transport::Tcp,
}; };
let mut stream; let mut stream;
{ {