From 4f96f0e17e85df380a250f6e25b4b38a240f2d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:33:01 -0300 Subject: [PATCH 01/21] src: lib: logger: Omit unused error variable --- src/lib/logger/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/logger/manager.rs b/src/lib/logger/manager.rs index 004b3d82..6b1b3165 100644 --- a/src/lib/logger/manager.rs +++ b/src/lib/logger/manager.rs @@ -151,7 +151,7 @@ pub fn init() { Ok(message) => { history.lock().unwrap().push(message); } - Err(error) => { + Err(_) => { tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } } From 86388f348b2435ed58f9a61f846711fad53ae82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 7 Nov 2024 12:16:43 -0300 Subject: [PATCH 02/21] src: lib: logger: Filter out unwanted logs from 3rd party crates --- src/lib/logger/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/logger/manager.rs b/src/lib/logger/manager.rs index 6b1b3165..8fb7a12b 100644 --- a/src/lib/logger/manager.rs +++ b/src/lib/logger/manager.rs @@ -142,7 +142,7 @@ pub fn init() { .with_target(false) .with_thread_ids(true) .with_thread_names(true) - .with_filter(server_env_filter); + .with_filter(filter_unwanted_crates(server_env_filter)); let history = HISTORY.clone(); MANAGER.lock().unwrap().process = Some(tokio::spawn(async move { From 11d195d2ed25a3e30861dfe7d50146f65cd674d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 15:39:18 -0300 Subject: [PATCH 03/21] cargo: Enable env clap create feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ceaa984d..a80e2d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ name = "mavlink-camera-manager" path = "src/main.rs" [dependencies] -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.5", features = ["derive", "env"] } regex = "1.10.4" #TODO: Investigate rweb to use openapi spec for free From 3903086528212b7b00cfc4d9dc2acd3d189317b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:28:55 -0300 Subject: [PATCH 04/21] src: lib: cli: Add onvif-auth CLI arg, with a fallback to MCM_ONVIF_AUTH environment variable --- src/lib/cli/manager.rs | 44 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/cli/manager.rs b/src/lib/cli/manager.rs index c80953ab..7ce1c8c7 100644 --- a/src/lib/cli/manager.rs +++ b/src/lib/cli/manager.rs @@ -1,6 +1,6 @@ use anyhow::Context; use clap; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use tracing::error; use crate::{custom, stream::gst::utils::PluginRankConfig}; @@ -101,6 +101,10 @@ struct Args { /// Sets the MAVLink System ID. #[arg(long, value_name = "SYSTEM_ID", default_value = "1")] mavlink_system_id: u8, + + /// Sets Onvif authentications. Alternatively, this can be passed as `MCM_ONVIF_AUTH` environment variable. + #[clap(long, value_name = "onvif://:@", value_delimiter = ',', value_parser = onvif_auth_validator, env = "MCM_ONVIF_AUTH")] + onvif_auth: Vec, } #[derive(Debug)] @@ -258,6 +262,34 @@ pub fn gst_feature_rank() -> Vec { .collect() } +pub fn onvif_auth() -> HashMap { + MANAGER + .clap_matches + .onvif_auth + .iter() + .filter_map(|val| { + let url = match url::Url::parse(val) { + Ok(url) => url, + Err(error) => { + error!("Failed parsing onvif auth url: {error:?}"); + return None; + } + }; + + let (host, credentials) = + match crate::controls::onvif::manager::Manager::credentials_from_url(&url) { + Ok((host, credentials)) => (host, credentials), + Err(error) => { + error!("Failed to get credentials from url {url}: {error:?}"); + return None; + } + }; + + Some((host, credentials)) + }) + .collect() +} + fn gst_feature_rank_validator(val: &str) -> Result { if let Some((_key, value_str)) = val.split_once('=') { if value_str.parse::().is_err() { @@ -279,6 +311,16 @@ fn turn_servers_validator(val: &str) -> Result { Ok(val.to_owned()) } +fn onvif_auth_validator(val: &str) -> Result { + let url = url::Url::parse(val).map_err(|e| format!("Failed parsing onvif auth url: {e:?}"))?; + + if !matches!(url.scheme().to_lowercase().as_str(), "onvif") { + return Err("Onvif authentication scheme should be \"onvif\"".to_owned()); + } + + Ok(val.to_owned()) +} + #[cfg(test)] mod tests { use super::*; From ea60de2e8d817e9ee5f5775cf53713c9c048a51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:31:33 -0300 Subject: [PATCH 05/21] src: lib: controls: onvif: camera: Split OnvifCamera shareable properties into a context struct --- src/lib/controls/onvif/camera.rs | 52 ++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index f4be9293..578605c6 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -1,13 +1,21 @@ +use std::sync::Arc; + use onvif::soap; use onvif_schema::{devicemgmt::GetDeviceInformationResponse, transport}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; +use tokio::sync::RwLock; use tracing::*; use crate::{stream::gst::utils::get_encode_from_rtspsrc, video::types::Format}; #[derive(Clone)] pub struct OnvifCamera { + pub context: Arc>, +} + +pub struct OnvifCameraContext { + pub streams_information: Option>, devicemgmt: soap::client::Client, event: Option, deviceio: Option, @@ -16,7 +24,6 @@ pub struct OnvifCamera { imaging: Option, ptz: Option, analytics: Option, - pub streams_information: Option>, } pub struct Auth { @@ -37,10 +44,13 @@ impl OnvifCamera { let devicemgmt_uri = &auth.url; let base_uri = &devicemgmt_uri.origin().ascii_serialization(); - let mut this = Self { - devicemgmt: soap::client::ClientBuilder::new(devicemgmt_uri) - .credentials(creds.clone()) - .build(), + let devicemgmt = soap::client::ClientBuilder::new(devicemgmt_uri) + .credentials(creds.clone()) + .build(); + + let mut context = OnvifCameraContext { + streams_information: None, + devicemgmt, imaging: None, ptz: None, event: None, @@ -48,11 +58,12 @@ impl OnvifCamera { media: None, media2: None, analytics: None, - streams_information: None, }; let services = - onvif_schema::devicemgmt::get_services(&this.devicemgmt, &Default::default()).await?; + onvif_schema::devicemgmt::get_services(&context.devicemgmt, &Default::default()) + .await + .context("Failed to get services")?; for service in &services.service { let service_url = url::Url::parse(&service.x_addr).map_err(anyhow::Error::msg)?; @@ -74,21 +85,24 @@ impl OnvifCamera { ); } } - "http://www.onvif.org/ver10/events/wsdl" => this.event = svc, - "http://www.onvif.org/ver10/deviceIO/wsdl" => this.deviceio = svc, - "http://www.onvif.org/ver10/media/wsdl" => this.media = svc, - "http://www.onvif.org/ver20/media/wsdl" => this.media2 = svc, - "http://www.onvif.org/ver20/imaging/wsdl" => this.imaging = svc, - "http://www.onvif.org/ver20/ptz/wsdl" => this.ptz = svc, - "http://www.onvif.org/ver20/analytics/wsdl" => this.analytics = svc, - _ => trace!("unknown service: {:?}", service), + "http://www.onvif.org/ver10/events/wsdl" => context.event = svc, + "http://www.onvif.org/ver10/deviceIO/wsdl" => context.deviceio = svc, + "http://www.onvif.org/ver10/media/wsdl" => context.media = svc, + "http://www.onvif.org/ver20/media/wsdl" => context.media2 = svc, + "http://www.onvif.org/ver20/imaging/wsdl" => context.imaging = svc, + "http://www.onvif.org/ver20/ptz/wsdl" => context.ptz = svc, + "http://www.onvif.org/ver20/analytics/wsdl" => context.analytics = svc, + _ => trace!("Unknwon service: {service:?}"), } } - this.streams_information - .replace(this.get_streams_information().await?); + let context = Arc::new(RwLock::new(context)); + + if let Err(error) = Self::update_streams_information(&context).await { + warn!("Failed to update streams information: {error:?}"); + } - Ok(this) + Ok(Self { context }) } #[instrument(level = "trace", skip(self))] From e9f02228e96597e3cbc236cf1cb1e64db6dc5297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:37:19 -0300 Subject: [PATCH 06/21] src: lib: controls: onvif: camera: Add device_information to context --- src/lib/controls/onvif/camera.rs | 33 +++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 578605c6..4cba50a8 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -1,9 +1,10 @@ use std::sync::Arc; use onvif::soap; -use onvif_schema::{devicemgmt::GetDeviceInformationResponse, transport}; +use onvif_schema::transport; use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use tracing::*; @@ -15,6 +16,7 @@ pub struct OnvifCamera { } pub struct OnvifCameraContext { + pub device_information: OnvifDeviceInformation, pub streams_information: Option>, devicemgmt: soap::client::Client, event: Option, @@ -37,6 +39,15 @@ pub struct OnvifStreamInformation { pub format: Format, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct OnvifDeviceInformation { + pub manufacturer: String, + pub model: String, + pub firmware_version: String, + pub serial_number: String, + pub hardware_id: String, +} + impl OnvifCamera { #[instrument(level = "trace", skip(auth))] pub async fn try_new(auth: &Auth) -> Result { @@ -48,7 +59,19 @@ impl OnvifCamera { .credentials(creds.clone()) .build(); + let device_information = + onvif_schema::devicemgmt::get_device_information(&devicemgmt, &Default::default()) + .await + .map(|i| OnvifDeviceInformation { + manufacturer: i.manufacturer, + model: i.model, + firmware_version: i.firmware_version, + serial_number: i.serial_number, + hardware_id: i.hardware_id, + })?; + let mut context = OnvifCameraContext { + device_information, streams_information: None, devicemgmt, imaging: None, @@ -105,14 +128,6 @@ impl OnvifCamera { Ok(Self { context }) } - #[instrument(level = "trace", skip(self))] - pub async fn get_device_information( - &self, - ) -> Result { - onvif_schema::devicemgmt::get_device_information(&self.devicemgmt, &Default::default()) - .await - } - async fn get_streams_information( &self, ) -> Result, transport::Error> { From a4d0663c6586cda717179bd7e021f34af11f6d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:38:53 -0300 Subject: [PATCH 07/21] src: lib: controls: onvif: camera: Implement update_streams_information --- src/lib/controls/onvif/camera.rs | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 4cba50a8..4a036581 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -128,6 +128,39 @@ impl OnvifCamera { Ok(Self { context }) } + #[instrument(level = "trace", skip_all)] + async fn update_streams_information(context: &Arc>) -> Result<()> { + let mut context = context.write().await; + + let media_client = context + .media + .clone() + .ok_or_else(|| transport::Error::Other("Client media is not available".into()))?; + + // Sometimes a camera responds empty, so we try a couple of times to improve our reliability + let mut tries = 10; + let new_streams_information = loop { + let new_streams_information = OnvifCamera::get_streams_information(&media_client) + .await + .context("Failed to get streams information")?; + + if new_streams_information.is_empty() { + if tries == 0 { + return Err(anyhow!("No streams information found")); + } + + tries -= 1; + continue; + } + + break new_streams_information; + }; + + context.streams_information.replace(new_streams_information); + + Ok(()) + } + async fn get_streams_information( &self, ) -> Result, transport::Error> { From 9f70406e618355d376849617cd1c1d2617f84262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:39:59 -0300 Subject: [PATCH 08/21] src: lib: controls: onvif: camera: Add OnvifDevice to OnvifCamera --- src/lib/controls/onvif/camera.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 4a036581..9679cfae 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -10,12 +10,15 @@ use tracing::*; use crate::{stream::gst::utils::get_encode_from_rtspsrc, video::types::Format}; +use super::manager::OnvifDevice; + #[derive(Clone)] pub struct OnvifCamera { pub context: Arc>, } pub struct OnvifCameraContext { + pub device: OnvifDevice, pub device_information: OnvifDeviceInformation, pub streams_information: Option>, devicemgmt: soap::client::Client, @@ -50,7 +53,7 @@ pub struct OnvifDeviceInformation { impl OnvifCamera { #[instrument(level = "trace", skip(auth))] - pub async fn try_new(auth: &Auth) -> Result { + pub async fn try_new(device: &OnvifDevice, auth: &Auth) -> Result { let creds = &auth.credentials; let devicemgmt_uri = &auth.url; let base_uri = &devicemgmt_uri.origin().ascii_serialization(); @@ -71,6 +74,7 @@ impl OnvifCamera { })?; let mut context = OnvifCameraContext { + device: device.clone(), device_information, streams_information: None, devicemgmt, From fc480c200400e08731741c05d0e031a4152288b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:40:19 -0300 Subject: [PATCH 09/21] src: lib: controls: onvif: camera: Instrument get_streams_information --- src/lib/controls/onvif/camera.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 9679cfae..97b7bc5a 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -165,6 +165,7 @@ impl OnvifCamera { Ok(()) } + #[instrument(level = "trace", skip_all)] async fn get_streams_information( &self, ) -> Result, transport::Error> { From 933671d6e0087a370f76490c24848d0587f65868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:41:16 -0300 Subject: [PATCH 10/21] src: lib: controls: onvif: camera: Make get_streams_information a class function --- src/lib/controls/onvif/camera.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 97b7bc5a..ad4786b4 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -167,14 +167,10 @@ impl OnvifCamera { #[instrument(level = "trace", skip_all)] async fn get_streams_information( - &self, + media_client: &soap::client::Client, ) -> Result, transport::Error> { let mut streams_information = vec![]; - let media_client = self - .media - .as_ref() - .ok_or_else(|| transport::Error::Other("Client media is not available".into()))?; let profiles = onvif_schema::media::get_profiles(media_client, &Default::default()) .await? .profiles; From 74eb39300a4b136b6a8e2149f91834777b009db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:43:21 -0300 Subject: [PATCH 11/21] src: lib: controls: onvif: camera: Add separation spaces, prefet implicit format --- src/lib/controls/onvif/camera.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index ad4786b4..183fb966 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -174,7 +174,8 @@ impl OnvifCamera { let profiles = onvif_schema::media::get_profiles(media_client, &Default::default()) .await? .profiles; - trace!("get_profiles response: {:#?}", &profiles); + trace!("get_profiles response: {profiles:#?}"); + let requests: Vec<_> = profiles .iter() .map( @@ -245,6 +246,7 @@ impl OnvifCamera { streams_information.push(OnvifStreamInformation { stream_uri, format }); } + Ok(streams_information) } } From 05706ced67e5d0a6f41529dc4c934871628fc9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 16:44:16 -0300 Subject: [PATCH 12/21] src: lib: controls: onvif: camera: Add last_update to OnvifCamera --- src/lib/controls/onvif/camera.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 183fb966..7d13363b 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -15,6 +15,7 @@ use super::manager::OnvifDevice; #[derive(Clone)] pub struct OnvifCamera { pub context: Arc>, + pub last_update: std::time::Instant, } pub struct OnvifCameraContext { @@ -129,7 +130,10 @@ impl OnvifCamera { warn!("Failed to update streams information: {error:?}"); } - Ok(Self { context }) + Ok(Self { + context, + last_update: std::time::Instant::now(), + }) } #[instrument(level = "trace", skip_all)] From 476eddba239b9fd5efa1cc4782f1bbdc6e088579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:14:01 -0300 Subject: [PATCH 13/21] src: lib: video: Add device_information to VideoSourceOnvif --- src/lib/video/video_source_onvif.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/video/video_source_onvif.rs b/src/lib/video/video_source_onvif.rs index 527f83ff..d0807bf8 100644 --- a/src/lib/video/video_source_onvif.rs +++ b/src/lib/video/video_source_onvif.rs @@ -1,7 +1,10 @@ use paperclip::actix::Apiv2Schema; use serde::{Deserialize, Serialize}; -use crate::controls::{onvif::manager::Manager as OnvifManager, types::Control}; +use crate::controls::{ + onvif::{camera::OnvifDeviceInformation, manager::Manager as OnvifManager}, + types::Control, +}; use super::{ types::*, @@ -17,6 +20,8 @@ pub enum VideoSourceOnvifType { pub struct VideoSourceOnvif { pub name: String, pub source: VideoSourceOnvifType, + #[serde(flatten)] + pub device_information: OnvifDeviceInformation, } impl VideoSourceFormats for VideoSourceOnvif { From 31ccfaf52ddb145f3bae904273c463bc513541fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 17:53:22 -0300 Subject: [PATCH 14/21] src: lib: controls: onvif: manager: Introduce OnvifDevice --- src/lib/controls/onvif/manager.rs | 64 +++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index e5e22d5b..4547ebee 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -1,10 +1,11 @@ -use std::collections::HashMap; -use std::sync::Arc; +use std::{collections::HashMap, net::Ipv4Addr, sync::Arc}; use anyhow::{anyhow, Context, Result}; use onvif::soap::client::Credentials; +use serde::Serialize; use tokio::sync::RwLock; use tracing::*; +use url::Url; use crate::video::{ types::{Format, VideoSourceType}, @@ -24,12 +25,42 @@ pub struct Manager { pub struct ManagerContext { cameras: HashMap, - /// Credentials can be either added in runtime, or passed via ENV or CLI args - credentials: HashMap>>, + /// Onvif devices discovered + discovered_devices: HashMap, } type StreamURI = String; -type Host = String; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct OnvifDevice { + pub uuid: uuid::Uuid, + pub ip: Ipv4Addr, + pub types: Vec, + pub hardware: Option, + pub name: Option, + pub urls: Vec, +} + +impl TryFrom for OnvifDevice { + type Error = anyhow::Error; + + fn try_from(device: onvif::discovery::Device) -> Result { + Ok(Self { + uuid: device_address_to_uuid(&device.address)?, + ip: device + .urls + .first() + .context("Device should have at least one URL")? + .host_str() + .context("Device URL should have a host")? + .parse()?, + types: device.types, + hardware: device.hardware, + name: device.name, + urls: device.urls, + }) + } +} impl Drop for Manager { fn drop(&mut self) { @@ -42,7 +73,7 @@ impl Default for Manager { fn default() -> Self { let mcontext = Arc::new(RwLock::new(ManagerContext { cameras: HashMap::new(), - credentials: HashMap::new(), + discovered_devices: HashMap::new(), })); let mcontext_clone = mcontext.clone(); @@ -73,7 +104,14 @@ impl Manager { ); } - #[instrument(level = "trace", skip(context))] + #[instrument(level = "debug")] + pub async fn onvif_devices() -> Vec { + let mcontext = MANAGER.read().await.mcontext.clone(); + let mcontext = mcontext.read().await; + + mcontext.discovered_devices.values().cloned().collect() + } + async fn discover_loop(context: Arc>) -> Result<()> { use futures::stream::StreamExt; use std::net::{IpAddr, Ipv4Addr}; @@ -188,3 +226,15 @@ impl Manager { .collect::>() } } + +/// Address must be something like `urn:uuid:bc071801-c50f-8301-ac36-bc071801c50f`. +/// Read 7 Device discovery from [ONVIF-Core-Specification](https://www.onvif.org/specs/core/ONVIF-Core-Specification-v1612a.pdf) +#[instrument(level = "debug")] +fn device_address_to_uuid(device_address: &str) -> Result { + device_address + .split(':') + .last() + .context("Failed to parse device address into a UUID")? + .parse() + .map_err(anyhow::Error::msg) +} From cc5b5f225f50dd7d89d6ab276611d73cb6db0791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 17:56:52 -0300 Subject: [PATCH 15/21] src: lib: controls: onvif: manager: change instrument level to debug --- src/lib/controls/onvif/manager.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index 4547ebee..82e2f4b0 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -69,7 +69,7 @@ impl Drop for Manager { } impl Default for Manager { - #[instrument(level = "trace")] + #[instrument(level = "debug")] fn default() -> Self { let mcontext = Arc::new(RwLock::new(ManagerContext { cameras: HashMap::new(), @@ -85,7 +85,7 @@ impl Default for Manager { impl Manager { // Construct our manager, should be done inside main - #[instrument(level = "trace")] + #[instrument(level = "debug")] pub async fn init() { MANAGER.as_ref(); @@ -187,7 +187,7 @@ impl Manager { } } - #[instrument(level = "trace")] + #[instrument(level = "debug")] pub async fn get_formats(stream_uri: &StreamURI) -> Result> { let mcontext = MANAGER.read().await.mcontext.clone(); let mcontext = mcontext.read().await; @@ -209,7 +209,7 @@ impl Manager { Ok(vec![stream_information.format.clone()]) } - #[instrument(level = "trace")] + #[instrument(level = "debug")] pub async fn streams_available() -> Vec { let mcontext = MANAGER.read().await.mcontext.clone(); let mcontext = mcontext.read().await; From 98548249ee649ec7237cb4adc08fae48130295cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 17:59:02 -0300 Subject: [PATCH 16/21] src: lib: controls: onvif: manager: Add cdoc for cameras property from ManagerContext --- src/lib/controls/onvif/manager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index 82e2f4b0..3065d52b 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -24,6 +24,7 @@ pub struct Manager { } pub struct ManagerContext { + /// Onvif Cameras cameras: HashMap, /// Onvif devices discovered discovered_devices: HashMap, From 6186c13482d4b4a20712b29e86a2cb043ae33359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:05:12 -0300 Subject: [PATCH 17/21] src: lib: controls: onvif: manager: Implement credentials --- src/lib/controls/onvif/manager.rs | 63 +++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index 3065d52b..f7b7fea4 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -28,6 +28,10 @@ pub struct ManagerContext { cameras: HashMap, /// Onvif devices discovered discovered_devices: HashMap, + /// Credentials can be either added in runtime, or loaded from settings + credentials: HashMap, + /// Credentials provided via url, such as those provided via ENV or CLI + url_credentials: HashMap, } type StreamURI = String; @@ -72,9 +76,14 @@ impl Drop for Manager { impl Default for Manager { #[instrument(level = "debug")] fn default() -> Self { + let url_credentials = crate::cli::manager::onvif_auth(); + dbg!(&url_credentials); + let mcontext = Arc::new(RwLock::new(ManagerContext { cameras: HashMap::new(), discovered_devices: HashMap::new(), + credentials: HashMap::new(), + url_credentials, })); let mcontext_clone = mcontext.clone(); @@ -89,20 +98,52 @@ impl Manager { #[instrument(level = "debug")] pub async fn init() { MANAGER.as_ref(); + } - let manager = MANAGER.write().await; + #[instrument(level = "debug", skip_all)] + pub async fn register_credentials( + device_uuid: uuid::Uuid, + credentials: Option, + ) -> Result<()> { + let mcontext = MANAGER.read().await.mcontext.clone(); + let mut mcontext = mcontext.write().await; + + match credentials { + Some(credentials) => { + let _ = mcontext.credentials.insert(device_uuid, credentials); + } + None => { + let _ = mcontext.credentials.remove(&device_uuid); + } + } - let mut mcontext = manager.mcontext.write().await; + Ok(()) + } + + #[instrument(level = "debug", skip_all)] + /// Expect onvif://:@:/, where path and port are ignored, and all the rest is mandatory. + pub fn credentials_from_url(url: &url::Url) -> Result<(Ipv4Addr, Credentials)> { + if url.scheme().ne("onvif") { + return Err(anyhow!("Scheme must be `onvif`")); + } - // TODO: fill MANAGER.context.credentials with credentials passed by ENV and CLI - // It can be in the form of ":@", but we need to escape special characters need to. - let _ = mcontext.credentials.insert( - "192.168.0.168".to_string(), - Arc::new(RwLock::new(Credentials { - username: "admin".to_string(), - password: "12345".to_string(), - })), - ); + let host = url + .host_str() + .context("Host must be provided")? + .parse::()?; + + let password = url + .password() + .context("Password must be provided")? + .to_string(); + + Ok(( + host, + Credentials { + username: url.username().to_string(), + password, + }, + )) } #[instrument(level = "debug")] From 41469328c9eaea0c77215cb2c2e0274f0f39336a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:08:29 -0300 Subject: [PATCH 18/21] src: lib: controls: onvif: manager: Reimplement onvif discover --- src/lib/controls/onvif/manager.rs | 164 +++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 50 deletions(-) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index f7b7fea4..5e0e82fb 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -154,10 +154,13 @@ impl Manager { mcontext.discovered_devices.values().cloned().collect() } - async fn discover_loop(context: Arc>) -> Result<()> { + #[instrument(level = "debug", skip(mcontext))] + async fn discover_loop(mcontext: Arc>) -> Result<()> { use futures::stream::StreamExt; use std::net::{IpAddr, Ipv4Addr}; + let scan_duration = tokio::time::Duration::from_secs(20); + loop { trace!("Discovering onvif..."); @@ -165,67 +168,102 @@ impl Manager { onvif::discovery::DiscoveryBuilder::default() .listen_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) - .duration(tokio::time::Duration::from_secs(20)) + .duration(scan_duration) .run() .await? .for_each_concurrent(MAX_CONCURRENT_JUMPERS, |device| { - let context = context.clone(); + let context = mcontext.clone(); async move { - trace!("Device found: {device:#?}"); - - for url in device.urls { - let host = url - .host() - .map(|host| host.to_string()) - .unwrap_or(url.to_string()); - - let credentials = if let Some(credentials) = - context.read().await.credentials.get(&host) - { - Some(credentials.read().await.clone()) - } else { - None - }; - - trace!("Device {host}. Using credentials: {credentials:?}"); - - let camera = match OnvifCamera::try_new(&Auth { - credentials: credentials.clone(), - url: url.clone(), - }) - .await - { - Ok(camera) => camera, - Err(error) => { - error!(host, "Failed creating camera: {error:?}"); - return; - } - }; - - let Some(streams_informations) = &camera.streams_information else { - error!(host, "Failed getting stream information"); - continue; - }; - - trace!(host, "Found streams {streams_informations:?}"); + let device: OnvifDevice = match device.try_into() { + Ok(onvif_device) => onvif_device, + Err(error) => { + error!("Failed parsing device: {error:?}"); + return; + } + }; + // Transfer from url_credentials to credentials. Note that `url_credentials` should never overwrride `credentials`, + // otherwise, the credentials set during runtime (via rest API) would not overwrride credentials passed via ENV/CLI. + { let mut context = context.write().await; - for stream_information in streams_informations { - context - .cameras - .entry(stream_information.stream_uri.to_string()) - .and_modify(|old_camera| *old_camera = camera.clone()) - .or_insert_with(|| { - debug!(host, "New stream inserted: {stream_information:?}"); - - camera.clone() - }); + if let Some(credential) = context.url_credentials.remove(&device.ip) { + trace!("Transferring credential: {credential:?}"); + let _ = + context.credentials.entry(device.uuid).or_insert(credential); } } + + Self::handle_device(context, device).await; } }) .await; + + // Remove old cameras + let mut mcontext = mcontext.write().await; + mcontext.cameras.retain(|stream_uri, camera| { + if camera.last_update.elapsed() > 3 * scan_duration { + debug!("Stream {stream_uri} removed after not being seen for too long"); + + return false; + } + + true + }); + } + } + + #[instrument(level = "debug", skip(mcontext))] + async fn handle_device(mcontext: Arc>, device: OnvifDevice) { + let _ = mcontext + .write() + .await + .discovered_devices + .insert(device.uuid, device.clone()); + + let credentials = mcontext.read().await.credentials.get(&device.uuid).cloned(); + + trace!("Device found, using credentials: {credentials:?}"); + + for url in &device.urls { + let camera = match OnvifCamera::try_new( + &device, + &Auth { + credentials: credentials.clone(), + url: url.clone(), + }, + ) + .await + { + Ok(camera) => camera, + Err(error) => { + let cause = error.root_cause(); + error!("Failed creating OnvifCamera: {error}: {cause}"); + continue; + } + }; + + let Some(streams_informations) = + &camera.context.read().await.streams_information.clone() + else { + error!("Failed getting stream information"); + continue; + }; + + trace!("Stream found: {streams_informations:?}"); + + let mut context = mcontext.write().await; + for stream_information in streams_informations { + context + .cameras + .entry(stream_information.stream_uri.to_string()) + .and_modify(|old_camera| *old_camera = camera.clone()) + .or_insert_with(|| { + trace!("New stream inserted: {stream_information:?}"); + + camera.clone() + }); + } } } @@ -267,6 +305,32 @@ impl Manager { }) .collect::>() } + + #[instrument(level = "debug")] + pub(crate) async fn remove_camera(device_uuid: uuid::Uuid) -> Result<()> { + let mcontext = MANAGER.read().await.mcontext.clone(); + + let mut cameras_to_remove = vec![]; + { + let mcontext = mcontext.read().await; + + for (stream_uri, camera) in mcontext.cameras.iter() { + if camera.context.read().await.device.uuid == device_uuid { + cameras_to_remove.push(stream_uri.clone()) + } + } + } + + { + let mut mcontext = mcontext.write().await; + + for stream_uri in cameras_to_remove { + let _ = mcontext.cameras.remove(&stream_uri); + } + } + + Ok(()) + } } /// Address must be something like `urn:uuid:bc071801-c50f-8301-ac36-bc071801c50f`. From 3b121a4ca1138b71c339098d7c2e6253a120af41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:11:01 -0300 Subject: [PATCH 19/21] src: lib: controls: onvif: manager: Update code related to camera context split --- src/lib/controls/onvif/manager.rs | 36 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/lib/controls/onvif/manager.rs b/src/lib/controls/onvif/manager.rs index 5e0e82fb..ebe0ed83 100644 --- a/src/lib/controls/onvif/manager.rs +++ b/src/lib/controls/onvif/manager.rs @@ -277,7 +277,8 @@ impl Manager { .get(stream_uri) .context("Camera not found")?; - let Some(streams_information) = &camera.streams_information else { + let Some(streams_information) = &camera.context.read().await.streams_information.clone() + else { return Err(anyhow!("Failed getting stream information")); }; @@ -294,16 +295,29 @@ impl Manager { let mcontext = MANAGER.read().await.mcontext.clone(); let mcontext = mcontext.read().await; - mcontext - .cameras - .keys() - .map(|stream_uri| { - VideoSourceType::Onvif(VideoSourceOnvif { - name: format!("{stream_uri}"), - source: VideoSourceOnvifType::Onvif(stream_uri.clone()), - }) - }) - .collect::>() + let mut streams_available = vec![]; + for (stream_uri, camera) in mcontext.cameras.iter() { + let device_information = camera.context.read().await.device_information.clone(); + + let name = format!( + "{model} - {manufacturer} ({hardware_id})", + model = device_information.model, + manufacturer = device_information.manufacturer, + hardware_id = device_information.hardware_id + ); + + let source = VideoSourceOnvifType::Onvif(stream_uri.clone()); + + let stream = VideoSourceType::Onvif(VideoSourceOnvif { + name, + source, + device_information, + }); + + streams_available.push(stream); + } + + streams_available } #[instrument(level = "debug")] From c3054289720f7a7c338531ddaaac1e79b0821a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 28 Nov 2024 18:14:39 -0300 Subject: [PATCH 20/21] src: lib: server: Add REST endpoints for onvif authentication --- src/lib/server/manager.rs | 9 ++++++ src/lib/server/pages.rs | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/lib/server/manager.rs b/src/lib/server/manager.rs index 72b2f890..0a5c66d5 100644 --- a/src/lib/server/manager.rs +++ b/src/lib/server/manager.rs @@ -92,6 +92,15 @@ pub async fn run(server_address: &str) -> Result<(), std::io::Error> { ) .route("", web::get().to(pages::thumbnail)), ) + .route("/onvif/devices", web::get().to(pages::onvif_devices)) + .route( + "/onvif/authentication", + web::post().to(pages::authenticate_onvif_device), + ) + .route( + "/onvif/authentication", + web::delete().to(pages::unauthenticate_onvif_device), + ) .build() }) .bind(server_address) diff --git a/src/lib/server/pages.rs b/src/lib/server/pages.rs index 53753745..3ee5cd95 100644 --- a/src/lib/server/pages.rs +++ b/src/lib/server/pages.rs @@ -116,6 +116,22 @@ impl Info { } } +#[derive(Apiv2Schema, Deserialize, Debug)] +pub struct AuthenticateOnvifDeviceRequest { + /// Onvif Device UUID, obtained via `/onvif/devices` get request + device_uuid: uuid::Uuid, + /// Username for the Onvif Device + username: String, + /// Password for the Onvif Device + password: String, +} + +#[derive(Apiv2Schema, Deserialize, Debug)] +pub struct UnauthenticateOnvifDeviceRequest { + /// Onvif Device UUID, obtained via `/onvif/devices` get request + device_uuid: uuid::Uuid, +} + use std::{ffi::OsStr, path::Path}; use include_dir::{include_dir, Dir}; @@ -500,3 +516,54 @@ pub async fn log(req: HttpRequest, stream: web::Payload) -> Result HttpResponse { + let onvif_devices = crate::controls::onvif::manager::Manager::onvif_devices().await; + + match serde_json::to_string_pretty(&onvif_devices) { + Ok(json) => HttpResponse::Ok() + .content_type("application/json") + .body(json), + Err(error) => HttpResponse::InternalServerError() + .content_type("text/plain") + .body(format!("{error:#?}")), + } +} + +#[api_v2_operation] +pub async fn authenticate_onvif_device( + query: web::Query, +) -> HttpResponse { + if let Err(error) = crate::controls::onvif::manager::Manager::register_credentials( + query.device_uuid, + Some(onvif::soap::client::Credentials { + username: query.username.clone(), + password: query.password.clone(), + }), + ) + .await + { + return HttpResponse::InternalServerError() + .content_type("text/plain") + .body(format!("{error:#?}")); + } + + HttpResponse::Ok().finish() +} + +#[api_v2_operation] +pub async fn unauthenticate_onvif_device( + query: web::Query, +) -> HttpResponse { + if let Err(error) = + crate::controls::onvif::manager::Manager::register_credentials(query.device_uuid, None) + .await + { + return HttpResponse::InternalServerError() + .content_type("text/plain") + .body(format!("{error:#?}")); + } + + HttpResponse::Ok().finish() +} From 11395679e43c34c43474bf83db78da16b54b0fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Mon, 9 Dec 2024 14:45:45 -0300 Subject: [PATCH 21/21] src: lib: controls: onvif: camera: Inject credentials into RTSP stream URI --- src/lib/controls/onvif/camera.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/lib/controls/onvif/camera.rs b/src/lib/controls/onvif/camera.rs index 7d13363b..b89ada25 100644 --- a/src/lib/controls/onvif/camera.rs +++ b/src/lib/controls/onvif/camera.rs @@ -22,6 +22,7 @@ pub struct OnvifCameraContext { pub device: OnvifDevice, pub device_information: OnvifDeviceInformation, pub streams_information: Option>, + pub credentials: Option, devicemgmt: soap::client::Client, event: Option, deviceio: Option, @@ -78,6 +79,7 @@ impl OnvifCamera { device: device.clone(), device_information, streams_information: None, + credentials: creds.clone(), devicemgmt, imaging: None, ptz: None, @@ -148,9 +150,10 @@ impl OnvifCamera { // Sometimes a camera responds empty, so we try a couple of times to improve our reliability let mut tries = 10; let new_streams_information = loop { - let new_streams_information = OnvifCamera::get_streams_information(&media_client) - .await - .context("Failed to get streams information")?; + let new_streams_information = + OnvifCamera::get_streams_information(&media_client, &context.credentials) + .await + .context("Failed to get streams information")?; if new_streams_information.is_empty() { if tries == 0 { @@ -172,6 +175,7 @@ impl OnvifCamera { #[instrument(level = "trace", skip_all)] async fn get_streams_information( media_client: &soap::client::Client, + credentials: &Option, ) -> Result, transport::Error> { let mut streams_information = vec![]; @@ -206,7 +210,7 @@ impl OnvifCamera { trace!("token={} name={}", &profile.token.0, &profile.name.0); trace!("\t{}", &stream_uri_response.media_uri.uri); - let stream_uri = match url::Url::parse(&stream_uri_response.media_uri.uri) { + let mut stream_uri = match url::Url::parse(&stream_uri_response.media_uri.uri) { Ok(stream_url) => stream_url, Err(error) => { error!( @@ -222,7 +226,25 @@ impl OnvifCamera { continue; }; + if let Some(credentials) = &credentials { + if stream_uri.set_username(&credentials.username).is_err() { + warn!("Failed setting username"); + continue; + } + + if stream_uri + .set_password(Some(&credentials.password)) + .is_err() + { + warn!("Failed setting password"); + continue; + } + + trace!("Using credentials {credentials:?}"); + } + let Some(encode) = get_encode_from_rtspsrc(&stream_uri).await else { + warn!("Failed getting encoding from RTSP stream at {stream_uri}"); continue; };