mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-26 13:58:43 +08:00
Compare commits
5 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f65f5812b2 | ||
|
|
b8f5b91168 | ||
|
|
6306f83316 | ||
|
|
96075fdf49 | ||
|
|
8c6dcf53a6 |
@@ -501,10 +501,10 @@ class MyTheme {
|
||||
)
|
||||
: null,
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(fontSize: 19),
|
||||
titleSmall: TextStyle(fontSize: 14),
|
||||
bodySmall: TextStyle(fontSize: 12, height: 1.25),
|
||||
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
|
||||
titleLarge: TextStyle(fontSize: 19, color: Colors.white70),
|
||||
titleSmall: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
bodySmall: TextStyle(fontSize: 12, color: Colors.white70, height: 1.25),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: Colors.white70, height: 1.25),
|
||||
labelLarge: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -1124,18 +1124,23 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
|
||||
Widget createDialogContent(String text) {
|
||||
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||
bool hasLink = linkRegExp.hasMatch(text);
|
||||
|
||||
// Early return: no link, use default theme color
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
final List<TextSpan> spans = [];
|
||||
int start = 0;
|
||||
bool hasLink = false;
|
||||
|
||||
linkRegExp.allMatches(text).forEach((match) {
|
||||
hasLink = true;
|
||||
if (match.start > start) {
|
||||
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||
}
|
||||
spans.add(TextSpan(
|
||||
text: match.group(0) ?? '',
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -1153,13 +1158,9 @@ Widget createDialogContent(String text) {
|
||||
spans.add(TextSpan(text: text.substring(start)));
|
||||
}
|
||||
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
children: spans,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -41,6 +42,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
final GlobalKey _keyboardKey = GlobalKey();
|
||||
double _keyboardHeight = 0;
|
||||
late bool _showTerminalExtraKeys;
|
||||
// For iOS edge swipe gesture
|
||||
double _swipeStartX = 0;
|
||||
double _swipeCurrentX = 0;
|
||||
|
||||
// For web only.
|
||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||
@@ -147,7 +151,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
return Scaffold(
|
||||
final scaffold = Scaffold(
|
||||
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Stack(
|
||||
@@ -192,9 +196,108 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
),
|
||||
),
|
||||
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
|
||||
// iOS-style circular close button in top-right corner
|
||||
if (isIOS) _buildCloseButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Add iOS edge swipe gesture to exit (similar to Android back button)
|
||||
if (isIOS) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final screenWidth = constraints.maxWidth;
|
||||
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
|
||||
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
|
||||
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
|
||||
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
|
||||
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
|
||||
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
|
||||
() => HorizontalDragGestureRecognizer(
|
||||
debugOwner: this,
|
||||
// Only respond to touch input, exclude mouse/trackpad
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
),
|
||||
(HorizontalDragGestureRecognizer instance) {
|
||||
instance
|
||||
// Capture initial touch-down position (before touch slop)
|
||||
..onDown = (details) {
|
||||
_swipeStartX = details.localPosition.dx;
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onUpdate = (details) {
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onEnd = (details) {
|
||||
// Check if swipe started from left edge and moved right
|
||||
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
|
||||
clientClose(sessionId, _ffi);
|
||||
}
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
}
|
||||
..onCancel = () {
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: scaffold,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
minimum: const EdgeInsets.only(
|
||||
top: 16, // iOS standard margin
|
||||
right: 16, // iOS standard margin
|
||||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: translate('Close'),
|
||||
child: Container(
|
||||
width: 44, // iOS standard tap target size
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // Half transparency
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
clientClose(sessionId, _ffi);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Close'),
|
||||
child: const Icon(
|
||||
Icons.chevron_left, // iOS-style back arrow
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingKeyboard() {
|
||||
|
||||
@@ -55,10 +55,6 @@ pub struct HwRamEncoder {
|
||||
pub pixfmt: AVPixelFormat,
|
||||
bitrate: u32, //kbs
|
||||
config: HwRamEncoderConfig,
|
||||
// Frame statistics for quality monitoring
|
||||
frame_count: u64,
|
||||
total_frame_size: u64,
|
||||
last_quality_log: std::time::Instant,
|
||||
}
|
||||
|
||||
impl EncoderApi for HwRamEncoder {
|
||||
@@ -98,35 +94,13 @@ impl EncoderApi for HwRamEncoder {
|
||||
}
|
||||
};
|
||||
match Encoder::new(ctx.clone()) {
|
||||
Ok(encoder) => {
|
||||
// Log detailed encoder information for diagnostics
|
||||
log::info!(
|
||||
"Hardware encoder created successfully: name='{}', format={:?}, resolution={}x{}, bitrate={} kbps, fps={}, gop={}, rate_control={:?}",
|
||||
config.name,
|
||||
format,
|
||||
config.width,
|
||||
config.height,
|
||||
bitrate,
|
||||
DEFAULT_FPS,
|
||||
gop,
|
||||
rc
|
||||
);
|
||||
// Log GPU signature for hardware-specific issue tracking
|
||||
let gpu_sig = hwcodec::common::get_gpu_signature();
|
||||
if !gpu_sig.is_empty() {
|
||||
log::info!("GPU signature: {}", gpu_sig);
|
||||
}
|
||||
Ok(HwRamEncoder {
|
||||
encoder,
|
||||
format,
|
||||
pixfmt: ctx.pixfmt,
|
||||
bitrate,
|
||||
config,
|
||||
frame_count: 0,
|
||||
total_frame_size: 0,
|
||||
last_quality_log: std::time::Instant::now(),
|
||||
})
|
||||
}
|
||||
Ok(encoder) => Ok(HwRamEncoder {
|
||||
encoder,
|
||||
format,
|
||||
pixfmt: ctx.pixfmt,
|
||||
bitrate,
|
||||
config,
|
||||
}),
|
||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||
}
|
||||
}
|
||||
@@ -197,7 +171,6 @@ impl EncoderApi for HwRamEncoder {
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let old_bitrate = self.bitrate;
|
||||
let mut bitrate = Self::bitrate(
|
||||
&self.config.name,
|
||||
self.config.width,
|
||||
@@ -208,22 +181,6 @@ impl EncoderApi for HwRamEncoder {
|
||||
bitrate = Self::check_bitrate_range(&self.config, bitrate);
|
||||
self.encoder.set_bitrate(bitrate as _).ok();
|
||||
self.bitrate = bitrate;
|
||||
|
||||
// Log quality changes for hardware-specific diagnostics
|
||||
if old_bitrate != bitrate {
|
||||
log::info!(
|
||||
"Hardware encoder quality changed: encoder='{}', ratio={:.2}, bitrate {} -> {} kbps",
|
||||
self.config.name,
|
||||
ratio,
|
||||
old_bitrate,
|
||||
bitrate
|
||||
);
|
||||
}
|
||||
|
||||
// Reset statistics on quality change
|
||||
self.frame_count = 0;
|
||||
self.total_frame_size = 0;
|
||||
self.last_quality_log = std::time::Instant::now();
|
||||
}
|
||||
self.config.quality = ratio;
|
||||
Ok(())
|
||||
@@ -277,43 +234,6 @@ impl HwRamEncoder {
|
||||
Ok(v) => {
|
||||
let mut data = Vec::<EncodeFrame>::new();
|
||||
data.append(v);
|
||||
|
||||
// Monitor encoding quality by tracking frame sizes
|
||||
if !data.is_empty() {
|
||||
self.frame_count += data.len() as u64;
|
||||
let frame_sizes: u64 = data.iter().map(|f| f.data.len() as u64).sum();
|
||||
self.total_frame_size += frame_sizes;
|
||||
|
||||
// Log quality statistics every 300 frames (10 seconds at 30fps)
|
||||
if self.frame_count % 300 == 0 && self.last_quality_log.elapsed().as_secs() >= 10 {
|
||||
let avg_frame_size = self.total_frame_size / self.frame_count;
|
||||
let expected_frame_size = (self.bitrate as u64 * 1000) / (8 * DEFAULT_FPS as u64);
|
||||
|
||||
// Log if actual frame size is significantly different from expected
|
||||
let ratio = avg_frame_size as f64 / expected_frame_size as f64;
|
||||
if ratio < 0.3 || ratio > 3.0 {
|
||||
log::warn!(
|
||||
"Hardware encoder quality issue detected: encoder='{}', avg_frame_size={} bytes, expected={} bytes, ratio={:.2}, bitrate={} kbps",
|
||||
self.config.name,
|
||||
avg_frame_size,
|
||||
expected_frame_size,
|
||||
ratio,
|
||||
self.bitrate
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Hardware encoder stats: encoder='{}', frames={}, avg_size={} bytes, expected={} bytes, ratio={:.2}",
|
||||
self.config.name,
|
||||
self.frame_count,
|
||||
avg_frame_size,
|
||||
expected_frame_size,
|
||||
ratio
|
||||
);
|
||||
}
|
||||
self.last_quality_log = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
Err(_) => Ok(Vec::<EncodeFrame>::new()),
|
||||
@@ -332,18 +252,6 @@ impl HwRamEncoder {
|
||||
Self::calc_bitrate(width, height, ratio, name.contains("h264"))
|
||||
}
|
||||
|
||||
/// Calculate bitrate for hardware encoders based on resolution and quality ratio.
|
||||
///
|
||||
/// NOTE: Hardware encoder quality can vary significantly across different GPUs/drivers.
|
||||
/// Some hardware may require higher bitrates than others to achieve acceptable quality.
|
||||
/// The multipliers below provide a baseline, but specific hardware (especially older
|
||||
/// GPUs or certain driver versions) may still produce poor quality output even with
|
||||
/// these settings. Monitor logs for "Hardware encoder quality issue detected" warnings.
|
||||
///
|
||||
/// If quality issues persist on specific hardware:
|
||||
/// - Check GPU driver version and update if needed
|
||||
/// - Consider forcing VP8/VP9 software codec as fallback
|
||||
/// - File bug report with GPU model and driver version
|
||||
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;
|
||||
@@ -356,21 +264,17 @@ impl HwRamEncoder {
|
||||
5.0
|
||||
}
|
||||
} else if h264 {
|
||||
// Increased base multiplier from 2.0 to 2.5 to improve image quality
|
||||
// while maintaining H264's compression efficiency
|
||||
if base > threshold {
|
||||
1.0 + 1.5 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
2.5
|
||||
}
|
||||
} else {
|
||||
// H265: Increased base multiplier from 1.5 to 2.0 to fix poor image quality
|
||||
// H265 should be more efficient than H264, but needs sufficient bitrate
|
||||
if base > threshold {
|
||||
1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
} else {
|
||||
if base > threshold {
|
||||
1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
1.5
|
||||
}
|
||||
};
|
||||
(base * factor) as u32
|
||||
}
|
||||
@@ -857,53 +761,3 @@ pub fn start_check_process() {
|
||||
std::thread::spawn(f);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_h264_h265_bitrate_calculation() {
|
||||
// Test with 1920x1080 resolution (base_bitrate() returns 2073 kbps for 1080p)
|
||||
let width = 1920;
|
||||
let height = 1080;
|
||||
|
||||
// Test with BR_BALANCED (0.67) - default quality setting
|
||||
let balanced_ratio = 0.67;
|
||||
let h264_balanced = HwRamEncoder::calc_bitrate(width, height, balanced_ratio, true);
|
||||
let h265_balanced = HwRamEncoder::calc_bitrate(width, height, balanced_ratio, false);
|
||||
|
||||
// H265 should get ~2777 kbps with new multiplier (was ~2084 with old 1.5x)
|
||||
assert!(h265_balanced >= 2700 && h265_balanced <= 2850,
|
||||
"H265 balanced bitrate should be ~2777 kbps, got {} kbps", h265_balanced);
|
||||
|
||||
// H264 should get ~3472 kbps with new multiplier (was ~2778 with old 2.0x)
|
||||
assert!(h264_balanced >= 3400 && h264_balanced <= 3550,
|
||||
"H264 balanced bitrate should be ~3472 kbps, got {} kbps", h264_balanced);
|
||||
|
||||
// H264 should have higher bitrate than H265 at same quality
|
||||
assert!(h264_balanced > h265_balanced,
|
||||
"H264 should have higher bitrate than H265");
|
||||
|
||||
// Test with BR_BEST (1.5) - best quality setting
|
||||
let best_ratio = 1.5;
|
||||
let h265_best = HwRamEncoder::calc_bitrate(width, height, best_ratio, false);
|
||||
|
||||
// At best quality, should use significantly more bitrate (>50% more)
|
||||
assert!((h265_best as f64) > (h265_balanced as f64 * 1.5),
|
||||
"Best quality should use >50% more bitrate than balanced");
|
||||
|
||||
// Test with BR_SPEED (0.5) - low quality setting
|
||||
let speed_ratio = 0.5;
|
||||
let h265_speed = HwRamEncoder::calc_bitrate(width, height, speed_ratio, false);
|
||||
|
||||
// At speed quality, should use less bitrate
|
||||
assert!(h265_speed < h265_balanced,
|
||||
"Speed quality should use less bitrate than balanced");
|
||||
|
||||
// Verify bitrate scales proportionally with resolution
|
||||
let hd_bitrate = HwRamEncoder::calc_bitrate(1280, 720, balanced_ratio, false);
|
||||
assert!(hd_bitrate < h265_balanced,
|
||||
"720p should use less bitrate than 1080p");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,15 +729,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"),
|
||||
("input note here", "輸入備註"),
|
||||
("note-at-conn-end-tip", "在連接結束時請求備註"),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Show terminal extra keys", "顯示終端機額外按鍵"),
|
||||
("Relative mouse mode", "相對滑鼠模式"),
|
||||
("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"),
|
||||
("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"),
|
||||
("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"),
|
||||
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
||||
("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"),
|
||||
("Changelog", "更新日誌"),
|
||||
("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"),
|
||||
("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user