use std::io::Read;
use std::sync::Arc;
use std::time::Duration;
use crate::camera::CameraClient;
use crate::config::ResolvedTarget;
const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(4);
pub trait CameraSource: Send + Sync {
fn configured(&self) -> bool;
fn snapshot(&self) -> Result<Vec<u8>, String>;
}
pub struct LiveCamera {
target: ResolvedTarget,
}
impl LiveCamera {
pub fn new(target: ResolvedTarget) -> Self {
Self { target }
}
}
impl CameraSource for LiveCamera {
fn configured(&self) -> bool {
true
}
fn snapshot(&self) -> Result<Vec<u8>, String> {
CameraClient::new(self.target.clone())
.with_timeout(SNAPSHOT_TIMEOUT)
.snapshot()
.map_err(|e| e.to_string())
}
}
pub struct NoCamera;
impl CameraSource for NoCamera {
fn configured(&self) -> bool {
false
}
fn snapshot(&self) -> Result<Vec<u8>, String> {
Err("no built-in camera".to_string())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExternalCamera {
pub label: String,
pub url: String,
pub stream_url: Option<String>,
pub park_tuning: Option<crate::core::park::ParkTuning>,
pub select_tuning: Option<crate::core::park::SelectTuning>,
}
impl ExternalCamera {
pub fn new(
label: Option<String>,
url: String,
stream_url: Option<String>,
index: usize,
) -> Self {
let label = label
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.unwrap_or_else(|| format!("external {}", index + 1));
let stream_url = stream_url
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
Self {
label,
url: url.trim().to_string(),
stream_url,
park_tuning: None,
select_tuning: None,
}
}
pub fn with_park_tuning(mut self, tuning: Option<crate::core::park::ParkTuning>) -> Self {
self.park_tuning = tuning;
self
}
pub fn with_select_tuning(mut self, tuning: Option<crate::core::park::SelectTuning>) -> Self {
self.select_tuning = tuning;
self
}
pub fn parse(entry: &str, index: usize) -> Option<Self> {
let entry = entry.trim();
if entry.is_empty() {
return None;
}
let (label, url) = if entry.starts_with("http://") || entry.starts_with("https://") {
(None, entry.to_string())
} else if let Some((l, u)) = entry.split_once('=') {
(Some(l.to_string()), u.to_string())
} else {
(None, entry.to_string())
};
Some(Self::new(label, url, None, index))
}
}
const STREAM_CONNECT_TIMEOUT: Duration = Duration::from_secs(8);
pub struct OpenedCameraStream {
pub content_type: String,
pub reader: Box<dyn Read + Send + 'static>,
}
pub type StreamOpen = Arc<dyn Fn() -> Result<OpenedCameraStream, String> + Send + Sync>;
pub fn open_mjpeg_stream(url: &str) -> Result<OpenedCameraStream, String> {
let agent = ureq::AgentBuilder::new()
.timeout_connect(STREAM_CONNECT_TIMEOUT)
.timeout_read(Duration::from_secs(30))
.redirects(0)
.build();
let resp = agent.get(url).call().map_err(|e| e.to_string())?;
let content_type = resp
.header("content-type")
.map(str::to_string)
.unwrap_or_else(|| "multipart/x-mixed-replace".to_string());
Ok(OpenedCameraStream {
content_type,
reader: resp.into_reader(),
})
}
pub fn url_stream_opener(url: String) -> StreamOpen {
Arc::new(move || open_mjpeg_stream(&url))
}