diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9b072e1bd41..7f6a3e61aef 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -891,7 +891,9 @@ pub fn allow_d3d_render() -> bool { } pub const BR_BEST: f32 = 1.5; -pub const BR_BALANCED: f32 = 0.67; +// Changed from 0.67 to 1.0 to increase default encoder bitrate +// This provides better quality separation between Best/Balanced/Speed modes +pub const BR_BALANCED: f32 = 1.0; pub const BR_SPEED: f32 = 0.5; #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 17eda7f3c2c..dd135c46173 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -253,30 +253,21 @@ impl HwRamEncoder { } pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 { - let base = base_bitrate(width as _, height as _) as f32 * ratio; - let threshold = 2000.0; - let decay_rate = 0.001; // 1000 * 0.001 = 1 + let base = base_bitrate(width as _, height as _) as f32; + // Monotonically increasing formula: factor = 1.0 + C / (1.0 + base / scale) + // d(bitrate)/d(base) = 1 + C/(1+base/scale)² > 0, always positive + // Hardware encoder needs higher bitrate for better fluency and quality + let scale = 2000.0; let factor: f32 = if cfg!(target_os = "android") { // https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3 - if base > threshold { - 1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate) - } else { - 5.0 - } + 1.0 + 4.0 / (1.0 + base / scale) } else if h264 { - if base > threshold { - 1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate) - } else { - 2.0 - } + 1.0 + 1.8 / (1.0 + base / scale) } else { - if base > threshold { - 1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate) - } else { - 1.5 - } + 1.0 + 1.2 / (1.0 + base / scale) }; - (base * factor) as u32 + // ratio is multiplied at the end for linear scaling with quality settings + (base * factor * ratio) as u32 } pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 { diff --git a/src/client.rs b/src/client.rs index a7b681ee1db..27b4eb8cfe9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1745,7 +1745,7 @@ pub struct LoginConfigHandler { pub save_ab_password_to_recent: bool, // true: connected with ab password pub other_server: Option<(String, String, String)>, pub custom_fps: Arc>>, - pub last_auto_fps: Option, + pub last_send_fps: Option, pub adapter_luid: Option, pub mark_unsupported: Vec, pub selected_windows_session_id: Option, @@ -2444,6 +2444,7 @@ impl LoginConfigHandler { self.save_config(config); } *self.custom_fps.lock().unwrap() = Some(fps as _); + self.last_send_fps = Some(fps as _); msg_out } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e0b3fcd6d03..36058ddca27 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1173,24 +1173,24 @@ impl Remote { if limited_fps > custom_fps { limited_fps = custom_fps; } - let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone(); + let last_send_fps = self.handler.lc.read().unwrap().last_send_fps.clone(); let displays = self.video_threads.keys().cloned().collect::>(); let mut fps_trending = |display: usize| { let thread = self.video_threads.get_mut(&display)?; let ctl = &mut thread.fps_control; let len = thread.video_queue.read().unwrap().len(); let decode_fps = thread.decode_fps.read().unwrap().clone()?; - let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _); + let last_send_fps = last_send_fps.clone().unwrap_or(custom_fps as _); if ctl.inactive_counter > inactive_threshold { return None; } - if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) { + if len > 1 && last_send_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) { ctl.idle_counter = 0; return Some(false); } if len <= 1 { ctl.idle_counter += 1; - if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps { + if ctl.idle_counter > 3 && last_send_fps + 3 <= limited_fps { return Some(true); } } @@ -1202,7 +1202,7 @@ impl Remote { let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect(); let should_decrease = trendings.iter().any(|v| *v == Some(false)); let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true)); - if last_auto_fps.is_none() || should_decrease || should_increase { + if last_send_fps.is_none() || should_decrease || should_increase { // limited_fps to ensure decoding is faster than encoding let mut auto_fps = limited_fps; if should_decrease && limited_fps < max_queue_len { @@ -1211,7 +1211,7 @@ impl Remote { if auto_fps < 1 { auto_fps = 1; } - if Some(auto_fps) != last_auto_fps { + if Some(auto_fps) != last_send_fps { let mut misc = Misc::new(); misc.set_option(OptionMessage { custom_fps: auto_fps as _, @@ -1221,7 +1221,7 @@ impl Remote { msg.set_misc(misc); self.sender.send(Data::Message(msg)).ok(); log::info!("Set fps to {}", auto_fps); - self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + self.handler.lc.write().unwrap().last_send_fps = Some(auto_fps); } } // send refresh diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index b02f6adf338..20c670c109e 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -29,20 +29,21 @@ delay: // Constants pub const FPS: u32 = 30; -pub const MIN_FPS: u32 = 1; +pub const MIN_FPS: u32 = 1; // Absolute minimum fps, used for final clamp +pub const MIN_AUTO_FPS: u32 = 5; // Minimum fps for auto adjustment, prevents fps from dropping too low pub const MAX_FPS: u32 = 120; pub const INIT_FPS: u32 = 15; // Bitrate ratio constants for different quality levels const BR_MAX: f32 = 40.0; // 2000 * 2 / 100 -const BR_MIN: f32 = 0.2; -const BR_MIN_HIGH_RESOLUTION: f32 = 0.1; // For high resolution, BR_MIN is still too high, so we set a lower limit +const BR_MIN: f32 = 0.2; // Absolute minimum bitrate ratio (user can set this low) const MAX_BR_MULTIPLE: f32 = 1.0; const HISTORY_DELAY_LEN: usize = 2; const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition +const NEW_CONNECTION_PROTECT_SECS: u64 = (2 * ADJUST_RATIO_INTERVAL) as u64; // Don't decrease bitrate within the first two ratio-adjust intervals of a new connection #[derive(Default, Debug, Clone)] struct UserDelay { @@ -157,7 +158,7 @@ impl VideoQoS { // Get current bitrate ratio with bounds checking pub fn ratio(&mut self) -> f32 { - if self.ratio < BR_MIN_HIGH_RESOLUTION || self.ratio > BR_MAX { + if self.ratio < BR_MIN || self.ratio > BR_MAX { self.ratio = BR_BALANCED; } self.ratio @@ -184,9 +185,13 @@ impl VideoQoS { impl VideoQoS { // Initialize new user session pub fn on_connection_open(&mut self, id: i32) { + // Only reset new_user_instant when first user connects, + // so a late-joining user doesn't block bitrate decreases for existing users. + if self.users.is_empty() { + self.new_user_instant = Instant::now(); + } self.users.insert(id, UserData::default()); self.abr_config = Config::get_option("enable-abr") != "N"; - self.new_user_instant = Instant::now(); } // Clean up user session @@ -321,7 +326,7 @@ impl VideoQoS { user.delay.quick_increase_fps_count = 0; } - fps = fps.clamp(MIN_FPS, highest_fps); + fps = fps.clamp(MIN_AUTO_FPS.min(highest_fps), highest_fps); // first network delay message adjust_ratio = user.delay.fps.is_none(); user.delay.fps = Some(fps); @@ -424,17 +429,10 @@ impl VideoQoS { }; let target_quality = self.latest_quality(); - let target_ratio = self.latest_quality().ratio(); + let target_ratio = target_quality.ratio(); let current_ratio = self.ratio; let current_bitrate = self.bitrate(); - // Calculate minimum ratio for high resolution (1Mbps baseline) - let ratio_1mbps = if current_bitrate > 0 { - Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION)) - } else { - None - }; - // Calculate ratio for adding 150kbps bandwidth let ratio_add_150kbps = if current_bitrate > 0 { Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32) @@ -443,29 +441,24 @@ impl VideoQoS { }; // Set minimum ratio based on quality mode + // Best(0.6) > Balanced(0.4) > Low(0.25) >= Custom(0.2~0.6) let min = match target_quality { - Quality::Best => { - // For Best quality, ensure minimum 1Mbps for high resolution - let mut min = BR_BEST / 2.5; - if let Some(ratio_1mbps) = ratio_1mbps { - if min > ratio_1mbps { - min = ratio_1mbps; - } - } - min.max(BR_MIN) - } - Quality::Balanced => { - let mut min = (BR_BALANCED / 2.0).min(0.4); - if let Some(ratio_1mbps) = ratio_1mbps { - if min > ratio_1mbps { - min = ratio_1mbps; - } + Quality::Best => BR_BEST / 2.5, + Quality::Balanced => BR_BALANCED / 2.5, + Quality::Low => BR_SPEED / 2.0, + Quality::Custom(v) => { + if v > BR_BEST { + BR_BEST / 2.5 + } else if v > BR_BALANCED { + BR_BALANCED / 2.5 + } else if v > BR_SPEED { + BR_SPEED / 2.0 + } else { + BR_MIN } - min.max(BR_MIN_HIGH_RESOLUTION) } - Quality::Low => BR_MIN_HIGH_RESOLUTION, - Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION, - }; + } + .max(BR_MIN); let max = target_ratio * MAX_BR_MULTIPLE; let mut v = current_ratio; @@ -503,7 +496,19 @@ impl VideoQoS { } } - self.ratio = v.clamp(min, max); + // First, clamp the newly computed ratio to respect configured bounds + let clamped = v.clamp(min.min(max), max); + + // Protect new connections from bitrate decrease due to network fluctuation. + // Compare against the current ratio also clamped to the same bounds, + // so user-initiated quality changes (e.g., Best -> Balanced) still take effect. + let new_connection_protection = + self.new_user_instant.elapsed().as_secs() < NEW_CONNECTION_PROTECT_SECS; + self.ratio = if new_connection_protection { + clamped.max(current_ratio.clamp(min.min(max), max)) + } else { + clamped + }; self.adjust_ratio_instant = Instant::now(); } @@ -519,8 +524,9 @@ impl VideoQoS { .unwrap_or(INIT_FPS); if self.users.iter().any(|u| u.1.delay.response_delayed) { - if fps > MIN_FPS + 1 { - fps = MIN_FPS + 1; + // Use MIN_AUTO_FPS instead of MIN_FPS for response delay fallback + if fps > MIN_AUTO_FPS + 1 { + fps = MIN_AUTO_FPS + 1; } } @@ -532,7 +538,7 @@ impl VideoQoS { } // Ensure fps stays within valid range - self.fps = fps.clamp(MIN_FPS, highest_fps); + self.fps = fps.clamp(MIN_FPS.min(highest_fps), highest_fps); } } @@ -546,7 +552,7 @@ struct RttCalculator { impl RttCalculator { const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples - const MIN_SAMPLES: usize = 10; // Require at least 10 samples + const MIN_SAMPLES: usize = 5; // Require at least 5 samples const ALPHA: f32 = 0.5; // Smoothing factor for weighted average /// Update RTT estimates with a new sample diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9ea0cba5b0d..5bf52bb2bcb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -474,14 +474,15 @@ impl Session { } pub fn save_image_quality(&self, value: String) { + let prev_image_quality = self.lc.read().unwrap().image_quality.clone(); let msg = self.lc.write().unwrap().save_image_quality(value.clone()); if let Some(msg) = msg { self.send(Data::Message(msg)); } - if value != "custom" { - let last_auto_fps = self.lc.read().unwrap().last_auto_fps; - if last_auto_fps.unwrap_or(usize::MAX) >= 30 { - // non custom quality use 30 fps + // When switching from custom to non-custom, reset fps to 30 if it was different + if prev_image_quality == "custom" && value != "custom" { + let last_send_fps = self.lc.read().unwrap().last_send_fps; + if last_send_fps.unwrap_or(usize::MAX) != 30 { let msg = self.lc.write().unwrap().set_custom_fps(30, false); self.send(Data::Message(msg)); }