130 lines
4.6 KiB
Rust
130 lines
4.6 KiB
Rust
#![forbid(unsafe_code)]
|
|
|
|
use std::time::Duration;
|
|
|
|
use crate::handshake::PeerCaps;
|
|
use crate::input::camera::{CameraCodec, CameraConfig};
|
|
|
|
/// Resolve the server address the client should dial first.
|
|
///
|
|
/// Inputs: process arguments after the executable name plus the optional
|
|
/// `LESAVKA_SERVER_ADDR` override from the environment.
|
|
/// Outputs: the address that should be used for both the handshake and the
|
|
/// long-lived RPC channels.
|
|
/// Why: keeping precedence rules pure makes startup behavior testable without
|
|
/// having to mutate the real process environment in every caller.
|
|
#[must_use]
|
|
pub fn resolve_server_addr(args: &[String], env_addr: Option<&str>) -> String {
|
|
args.first()
|
|
.cloned()
|
|
.or_else(|| env_addr.map(ToOwned::to_owned))
|
|
.unwrap_or_else(|| "http://127.0.0.1:50051".to_string())
|
|
}
|
|
|
|
/// Convert handshake metadata into a local camera capture configuration.
|
|
///
|
|
/// Inputs: the negotiated peer capabilities reported by the server.
|
|
/// Outputs: `Some(CameraConfig)` only when the server advertised a complete
|
|
/// camera profile that the client can honor locally.
|
|
/// Why: camera startup should fail closed when the negotiated profile is
|
|
/// incomplete, rather than guessing a codec or frame size on the client.
|
|
#[must_use]
|
|
pub fn camera_config_from_caps(caps: &PeerCaps) -> Option<CameraConfig> {
|
|
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?,
|
|
})
|
|
}
|
|
|
|
/// Clamp the video queue size to a sensible minimum.
|
|
///
|
|
/// Inputs: the operator-provided queue depth, if any.
|
|
/// Outputs: a queue depth that is always large enough to absorb short render
|
|
/// stalls without turning the GUI thread into a drop storm.
|
|
/// Why: the render loop is bursty under GTK/winit, so tiny queues create
|
|
/// needless packet churn and noisy logs.
|
|
#[must_use]
|
|
pub fn sanitize_video_queue(queue: Option<usize>) -> usize {
|
|
queue.unwrap_or(256).max(16)
|
|
}
|
|
|
|
/// Pick the next reconnect delay for camera and microphone streams.
|
|
///
|
|
/// Inputs: the current retry delay.
|
|
/// Outputs: an exponential backoff capped at 30 seconds.
|
|
/// Why: repeated reconnect failures should back off quickly without stalling
|
|
/// recovery for minutes after a transient outage clears.
|
|
#[must_use]
|
|
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<CameraCodec> {
|
|
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::{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(&[], Some("http://env:2")),
|
|
"http://env:2"
|
|
);
|
|
assert_eq!(resolve_server_addr(&[], None), "http://127.0.0.1:50051");
|
|
}
|
|
|
|
#[test]
|
|
fn camera_config_from_caps_requires_complete_profile() {
|
|
let mut caps = PeerCaps {
|
|
camera: true,
|
|
microphone: false,
|
|
camera_output: Some(String::from("uvc")),
|
|
camera_codec: Some(String::from("mjpeg")),
|
|
camera_width: Some(1280),
|
|
camera_height: Some(720),
|
|
camera_fps: Some(25),
|
|
};
|
|
|
|
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), 256);
|
|
assert_eq!(sanitize_video_queue(Some(8)), 16);
|
|
assert_eq!(sanitize_video_queue(Some(512)), 512);
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
}
|