From 1c9a55653dc0a760559f3decf2f77d045eac4d2d Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Wed, 27 Oct 2021 14:27:23 -0700 Subject: [PATCH] allow setting rtsp transport per-stream --- CHANGELOG.md | 1 + server/db/json.rs | 4 ++++ server/src/cmds/config/cameras.rs | 40 +++++++++++++++++++++++++++---- server/src/cmds/run.rs | 4 +++- server/src/streamer.rs | 23 +++++++++++++++--- 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f002d0..ad81ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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) diff --git a/server/db/json.rs b/server/db/json.rs index 6a5f284..0c6d131 100644 --- a/server/db/json.rs +++ b/server/db/json.rs @@ -209,6 +209,10 @@ pub struct StreamConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, + /// 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 /// currently-recording file. /// diff --git a/server/src/cmds/config/cameras.rs b/server/src/cmds/config/cameras.rs index a541044..83a8e26 100644 --- a/server/src/cmds/config/cameras.rs +++ b/server/src/cmds/config/cameras.rs @@ -13,6 +13,7 @@ use std::collections::BTreeMap; use std::sync::Arc; use url::Url; +#[derive(Debug)] struct Camera { short_name: String, description: String, @@ -22,11 +23,12 @@ struct Camera { streams: [Stream; db::NUM_STREAM_TYPES], } -#[derive(Default)] +#[derive(Debug, Default)] struct Stream { url: String, record: bool, flush_if_sec: String, + rtsp_transport: &'static str, sample_file_dir_id: Option, } @@ -82,6 +84,11 @@ fn get_camera(siv: &mut Cursive) -> Camera { .find_name::(&format!("{}_record", t.as_str())) .unwrap() .is_checked(); + let rtsp_transport = *siv + .find_name::>(&format!("{}_rtsp_transport", t.as_str())) + .unwrap() + .selection() + .unwrap(); let flush_if_sec = siv .find_name::(&format!("{}_flush_if_sec", t.as_str())) .unwrap() @@ -97,9 +104,11 @@ fn get_camera(siv: &mut Cursive) -> Camera { url, record, flush_if_sec, + rtsp_transport, sample_file_dir_id, }; } + log::trace!("camera is: {:#?}", &camera); camera } @@ -155,6 +164,7 @@ fn press_edit(siv: &mut Cursive, db: &Arc, id: Option) { }) .to_owned(); 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.config.flush_if_sec = if stream.flush_if_sec.is_empty() { 0 @@ -175,9 +185,13 @@ fn press_edit(siv: &mut Cursive, db: &Arc, id: Option) { })(); if let Err(e) = result { siv.add_layer( - views::Dialog::text(format!("Unable to add camera: {}", e)) - .title("Error") - .dismiss_button("Abort"), + views::Dialog::text(format!( + "Unable to {} camera: {}", + if id.is_some() { "edit" } else { "add" }, + e + )) + .title("Error") + .dismiss_button("Abort"), ); } else { siv.pop_layer(); // get rid of the add/edit camera dialog. @@ -457,6 +471,13 @@ fn edit_camera_dialog(db: &Arc, siv: &mut Cursive, item: &Option::new() + .with_all([("(default)", ""), ("tcp", "tcp"), ("udp", "udp")]) + .popup() + .with_name(format!("{}_rtsp_transport", type_.as_str())), + ) .child( "flush_if_sec", views::EditView::new().with_name(format!("{}_flush_if_sec", type_.as_str())), @@ -529,11 +550,22 @@ fn edit_camera_dialog(db: &Arc, siv: &mut Cursive, item: &Option| { + v.set_selection(match s.config.rtsp_transport.as_str() { + "tcp" => 1, + "udp" => 2, + _ => 0, + }) + }, + ); dialog.call_on_name( &format!("{}_flush_if_sec", t.as_str()), |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( &format!("{}_sample_file_dir", t.as_str()), |v: &mut views::SelectView>| v.set_selection(selected_dir), diff --git a/server/src/cmds/run.rs b/server/src/cmds/run.rs index 4e27277..a414be0 100644 --- a/server/src/cmds/run.rs +++ b/server/src/cmds/run.rs @@ -76,6 +76,8 @@ pub struct Args { #[structopt(long, default_value = "retina", parse(try_from_str))] 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)] rtsp_transport: retina::client::Transport, } @@ -265,7 +267,7 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result>, pub shutdown_rx: &'tmp base::shutdown::Receiver, } @@ -71,6 +72,22 @@ where if !url.username().is_empty() || url.password().is_some() { 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 { shutdown_rx: env.shutdown_rx.clone(), rotate_offset_sec, @@ -79,7 +96,7 @@ where dir, syncer_channel, opener: env.opener, - transport: env.transport, + transport: stream_transport.unwrap_or(env.default_transport), stream_id, session_group, short_name: format!("{}-{}", c.short_name, s.type_.as_str()), @@ -417,7 +434,7 @@ mod tests { opener: &opener, db: &db.db, shutdown_rx: &shutdown_rx, - transport: retina::client::Transport::Tcp, + default_transport: retina::client::Transport::Tcp, }; let mut stream; {