Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion libs/scrap/src/common/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
29 changes: 10 additions & 19 deletions libs/scrap/src/common/hwcodec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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
}

Expand Down
14 changes: 7 additions & 7 deletions src/client/io_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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 {
Expand All @@ -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 _,
Expand All @@ -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
Expand Down
84 changes: 45 additions & 39 deletions src/server/video_qos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -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;
}
}

Expand All @@ -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);
}
}

Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/ui_session_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down