#![forbid(unsafe_code)] use std::time::Duration; use crate::handshake::PeerCaps; use crate::input::camera::{CameraCodec, CameraConfig}; pub const DEFAULT_SERVER_ADDR: &str = "https://38.28.125.112:50051"; #[must_use] /// Resolve the server address from `--server`, positional args, env, or default. pub fn resolve_server_addr(args: &[String], env_addr: Option<&str>) -> String { args.windows(2) .find_map(|pair| { (pair[0] == "--server" && !pair[1].starts_with("--")).then(|| pair[1].clone()) }) .or_else(|| args.iter().find(|arg| !arg.starts_with("--")).cloned()) .or_else(|| env_addr.map(ToOwned::to_owned)) .unwrap_or_else(|| DEFAULT_SERVER_ADDR.to_string()) } #[must_use] /// Build local camera capture settings from negotiated peer capabilities. pub fn camera_config_from_caps(caps: &PeerCaps) -> Option { let codec = parse_camera_codec(caps.camera_codec.as_deref()?)?; Some(CameraConfig { codec, width: caps.camera_width?, height: caps.camera_height?, fps: caps.camera_fps?, }) } #[must_use] /// Clamp queue depth so stale video cannot backlog behind live input. pub fn sanitize_video_queue(queue: Option) -> usize { queue.unwrap_or(8).max(4) } #[must_use] /// Exponential reconnect delay capped at 30 seconds. pub fn next_delay(current: Duration) -> Duration { match current.as_secs() { 1..=15 => current * 2, _ => Duration::from_secs(30), } } fn parse_camera_codec(raw: &str) -> Option { match raw.trim().to_ascii_lowercase().as_str() { "mjpeg" | "mjpg" | "jpeg" => Some(CameraCodec::Mjpeg), "h264" => Some(CameraCodec::H264), _ => None, } } #[cfg(test)] mod tests { use super::{ DEFAULT_SERVER_ADDR, camera_config_from_caps, next_delay, resolve_server_addr, sanitize_video_queue, }; use crate::handshake::PeerCaps; use crate::input::camera::CameraCodec; use std::time::Duration; #[test] fn resolve_server_addr_prefers_cli_then_env_then_default() { assert_eq!( resolve_server_addr(&[String::from("http://cli:1")], Some("http://env:2")), "http://cli:1" ); assert_eq!( resolve_server_addr( &[ String::from("--no-launcher"), String::from("--server"), String::from("http://cli-flag:3"), ], Some("http://env:2"), ), "http://cli-flag:3" ); assert_eq!( resolve_server_addr(&[String::from("--launcher")], Some("http://env:2")), "http://env:2" ); assert_eq!( resolve_server_addr(&[], Some("http://env:2")), "http://env:2" ); assert_eq!(resolve_server_addr(&[], None), DEFAULT_SERVER_ADDR); } #[test] fn camera_config_from_caps_requires_complete_profile() { let mut caps = PeerCaps { camera: true, microphone: false, server_version: None, camera_output: Some(String::from("uvc")), camera_codec: Some(String::from("mjpeg")), camera_width: Some(1280), camera_height: Some(720), camera_fps: Some(25), eye_width: None, eye_height: None, eye_fps: None, }; let config = camera_config_from_caps(&caps).expect("complete caps should map"); assert!(matches!(config.codec, CameraCodec::Mjpeg)); assert_eq!(config.width, 1280); caps.camera_codec = Some(String::from("vp9")); assert!(camera_config_from_caps(&caps).is_none()); } #[test] fn sanitize_video_queue_enforces_floor() { assert_eq!(sanitize_video_queue(None), 8); assert_eq!(sanitize_video_queue(Some(1)), 4); assert_eq!(sanitize_video_queue(Some(32)), 32); } #[test] fn next_delay_doubles_until_capped() { assert_eq!(next_delay(Duration::from_secs(1)), Duration::from_secs(2)); assert_eq!(next_delay(Duration::from_secs(15)), Duration::from_secs(30)); assert_eq!(next_delay(Duration::from_secs(31)), Duration::from_secs(30)); } }