diff --git a/client/Cargo.toml b/client/Cargo.toml index a7987e1..bc6cfae 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.11.3" +version = "0.11.4" edition = "2024" [dependencies] diff --git a/client/src/launcher/preview.rs b/client/src/launcher/preview.rs index b6742ae..628dbe3 100644 --- a/client/src/launcher/preview.rs +++ b/client/src/launcher/preview.rs @@ -32,6 +32,14 @@ const PREVIEW_WIDTH: i32 = 640; #[cfg(not(coverage))] const PREVIEW_HEIGHT: i32 = 360; #[cfg(not(coverage))] +const INLINE_PREVIEW_REQUEST_WIDTH: i32 = 960; +#[cfg(not(coverage))] +const INLINE_PREVIEW_REQUEST_HEIGHT: i32 = 540; +#[cfg(not(coverage))] +const INLINE_PREVIEW_REQUEST_FPS: u32 = 24; +#[cfg(not(coverage))] +const INLINE_PREVIEW_MAX_KBIT: u32 = 4_000; +#[cfg(not(coverage))] const DEFAULT_EYE_SOURCE_WIDTH: i32 = 1920; #[cfg(not(coverage))] const DEFAULT_EYE_SOURCE_HEIGHT: i32 = 1080; @@ -105,14 +113,20 @@ impl PreviewSurface { display_height: preview_dimension("LESAVKA_PREVIEW_HEIGHT", PREVIEW_HEIGHT), requested_width: preview_dimension( "LESAVKA_PREVIEW_REQUEST_WIDTH", - DEFAULT_EYE_SOURCE_WIDTH, + INLINE_PREVIEW_REQUEST_WIDTH, ), requested_height: preview_dimension( "LESAVKA_PREVIEW_REQUEST_HEIGHT", - DEFAULT_EYE_SOURCE_HEIGHT, + INLINE_PREVIEW_REQUEST_HEIGHT, + ), + requested_fps: preview_bitrate( + "LESAVKA_PREVIEW_REQUEST_FPS", + INLINE_PREVIEW_REQUEST_FPS, + ), + max_bitrate_kbit: preview_bitrate( + "LESAVKA_PREVIEW_MAX_KBIT", + INLINE_PREVIEW_MAX_KBIT, ), - requested_fps: preview_bitrate("LESAVKA_PREVIEW_REQUEST_FPS", 30), - max_bitrate_kbit: preview_bitrate("LESAVKA_PREVIEW_MAX_KBIT", 12_000), prefer_reencode: true, }, Self::Window => PreviewProfile { @@ -267,15 +281,28 @@ impl LauncherPreview { max_bitrate_kbit: u32, prefer_reencode: bool, ) { + let ( + inline_requested_width, + inline_requested_height, + inline_requested_fps, + inline_max_bitrate_kbit, + inline_prefer_reencode, + ) = adapt_inline_preview_request( + requested_width, + requested_height, + requested_fps, + max_bitrate_kbit, + prefer_reencode, + ); self.rebuild_feed( &self.inline_feeds, monitor_id, Some(( - requested_width, - requested_height, - requested_fps, - max_bitrate_kbit, - prefer_reencode, + inline_requested_width, + inline_requested_height, + inline_requested_fps, + inline_max_bitrate_kbit, + inline_prefer_reencode, )), None, ); @@ -1307,6 +1334,80 @@ fn preview_dimension(var: &str, default: i32) -> i32 { .unwrap_or(default) } +#[cfg(not(coverage))] +fn adapt_inline_preview_request( + requested_width: i32, + requested_height: i32, + requested_fps: u32, + max_bitrate_kbit: u32, + prefer_reencode: bool, +) -> (i32, i32, u32, u32, bool) { + let bounded_width = requested_width.max(2).min(preview_dimension( + "LESAVKA_PREVIEW_REQUEST_WIDTH", + INLINE_PREVIEW_REQUEST_WIDTH, + )); + let bounded_height = requested_height.max(2).min(preview_dimension( + "LESAVKA_PREVIEW_REQUEST_HEIGHT", + INLINE_PREVIEW_REQUEST_HEIGHT, + )); + let (inline_width, inline_height) = fit_preview_within_bounds( + requested_width.max(2), + requested_height.max(2), + bounded_width, + bounded_height, + ); + let inline_fps = requested_fps.max(1).min(preview_bitrate( + "LESAVKA_PREVIEW_REQUEST_FPS", + INLINE_PREVIEW_REQUEST_FPS, + )); + let inline_bitrate = max_bitrate_kbit.max(800).min(preview_bitrate( + "LESAVKA_PREVIEW_MAX_KBIT", + INLINE_PREVIEW_MAX_KBIT, + )); + let adapted = inline_width != requested_width.max(2) + || inline_height != requested_height.max(2) + || inline_fps != requested_fps.max(1) + || inline_bitrate != max_bitrate_kbit.max(800); + ( + inline_width, + inline_height, + inline_fps, + inline_bitrate, + prefer_reencode || adapted, + ) +} + +#[cfg(not(coverage))] +fn fit_preview_within_bounds( + requested_width: i32, + requested_height: i32, + max_width: i32, + max_height: i32, +) -> (i32, i32) { + let requested_width = round_down_even_i32(requested_width.max(2)); + let requested_height = round_down_even_i32(requested_height.max(2)); + let max_width = round_down_even_i32(max_width.max(2)); + let max_height = round_down_even_i32(max_height.max(2)); + if requested_width <= max_width && requested_height <= max_height { + return (requested_width, requested_height); + } + let width_limited_height = + round_down_even_i32((requested_height * max_width) / requested_width); + if width_limited_height <= max_height { + (max_width, width_limited_height.max(2)) + } else { + let height_limited_width = + round_down_even_i32((requested_width * max_height) / requested_height); + (height_limited_width.max(2), max_height) + } +} + +#[cfg(not(coverage))] +fn round_down_even_i32(value: i32) -> i32 { + let value = value.max(2); + value - (value & 1) +} + #[cfg(not(coverage))] fn events_per_second(events: &VecDeque, now: Instant) -> f32 { let Some(oldest) = events.front().copied() else { @@ -1362,20 +1463,22 @@ fn compute_peak_gap_ms(samples: &VecDeque<(Instant, f32)>) -> f32 { #[cfg(test)] mod tests { use super::{ - DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, PREVIEW_HEIGHT, PREVIEW_WIDTH, - PreviewSurface, PreviewTelemetry, + DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT, + INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH, + PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry, + adapt_inline_preview_request, }; use std::time::{Duration, Instant}; #[test] - fn inline_preview_profile_uses_existing_defaults() { + fn inline_preview_profile_uses_lightweight_defaults() { let profile = PreviewSurface::Inline.profile(); assert_eq!(profile.display_width, PREVIEW_WIDTH); assert_eq!(profile.display_height, PREVIEW_HEIGHT); - assert_eq!(profile.requested_width, DEFAULT_EYE_SOURCE_WIDTH); - assert_eq!(profile.requested_height, DEFAULT_EYE_SOURCE_HEIGHT); - assert_eq!(profile.requested_fps, 30); - assert_eq!(profile.max_bitrate_kbit, 12_000); + assert_eq!(profile.requested_width, INLINE_PREVIEW_REQUEST_WIDTH); + assert_eq!(profile.requested_height, INLINE_PREVIEW_REQUEST_HEIGHT); + assert_eq!(profile.requested_fps, INLINE_PREVIEW_REQUEST_FPS); + assert_eq!(profile.max_bitrate_kbit, INLINE_PREVIEW_MAX_KBIT); } #[test] @@ -1389,6 +1492,12 @@ mod tests { assert_eq!(profile.max_bitrate_kbit, 12_000); } + #[test] + fn inline_preview_request_caps_large_full_quality_streams() { + let adapted = adapt_inline_preview_request(1920, 1080, 30, 12_000, false); + assert_eq!(adapted, (960, 540, 24, 4_000, true)); + } + #[test] fn preview_telemetry_reports_fps_jitter_loss_and_drop_metrics() { let mut telemetry = PreviewTelemetry::default(); diff --git a/common/Cargo.toml b/common/Cargo.toml index e690d76..6e20c42 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.11.3" +version = "0.11.4" edition = "2024" build = "build.rs" diff --git a/common/src/cli.rs b/common/src/cli.rs index 9d28050..5ee1fc0 100644 --- a/common/src/cli.rs +++ b/common/src/cli.rs @@ -17,6 +17,6 @@ mod tests { #[test] fn banner_includes_version() { - assert_eq!(banner("0.11.3"), "lesavka-common CLI (v0.11.3)"); + assert_eq!(banner("0.11.4"), "lesavka-common CLI (v0.11.4)"); } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 46c7fe6..b97f6d9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.11.3" +version = "0.11.4" edition = "2024" autobins = false