Increase default bitrate and optimize QoS for better image quality

- Increase BR_BALANCED from 0.67 to 1.0 for higher default bitrate
  - Refactor calc_bitrate formula to be monotonically increasing
    (old formula had non-monotonic region near threshold)
  - Add new connection protection: don't decrease bitrate in first 6 seconds
  - Add BR_MIN_AUTO (0.3) and MIN_AUTO_FPS (5) for auto adjustment limits
  - Reduce RttCalculator::MIN_SAMPLES from 10 to 5 for faster stabilization

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages 2026-02-06 11:31:11 +08:00
parent 6306f83316
commit 8fa9e8294d
6 changed files with 62 additions and 69 deletions

View file

@ -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)]

View file

@ -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 + 6.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 {

View file

@ -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<Mutex<Option<usize>>>,
pub last_auto_fps: Option<usize>,
pub last_send_fps: Option<usize>,
pub adapter_luid: Option<i64>,
pub mark_unsupported: Vec<CodecFormat>,
pub selected_windows_session_id: Option<u32>,
@ -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
}

View file

@ -1173,24 +1173,24 @@ impl<T: InvokeUiSession> Remote<T> {
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::<Vec<_>>();
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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

View file

@ -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 = RttCalculator::MIN_SAMPLES as u64 + 1; // Don't decrease bitrate within first N seconds of 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
@ -321,7 +322,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);
@ -428,13 +429,6 @@ impl VideoQoS {
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 +437,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;
}
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)
}
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;
}
}
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 +492,15 @@ impl VideoQoS {
}
}
self.ratio = v.clamp(min, max);
// Protect new connections from bitrate decrease due to network fluctuation.
// User-initiated quality changes (e.g., Best -> Balanced) will still take effect.
let new_connection_protection =
self.new_user_instant.elapsed().as_secs() < NEW_CONNECTION_PROTECT_SECS;
if new_connection_protection && v < current_ratio {
v = current_ratio;
}
self.ratio = v.clamp(min.min(max), max);
self.adjust_ratio_instant = Instant::now();
}
@ -519,8 +516,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 +530,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 +544,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

View file

@ -474,14 +474,15 @@ impl<T: InvokeUiSession> Session<T> {
}
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));
}