mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-25 20:18:47 +08:00
Compare commits
8 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de11e95598 | ||
|
|
b3957febe1 | ||
|
|
eacfdaed61 | ||
|
|
8c8e6deb18 | ||
|
|
22000245bc | ||
|
|
e1b1a927b8 | ||
|
|
1e6bfa7bb1 | ||
|
|
79ef4c4501 |
@@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
// For mouse mode, we need to block the events when the cursor is in a blocked area.
|
||||
// So we need to cache the last tap down position.
|
||||
Offset? _lastTapDownPositionForMouseMode;
|
||||
// Cache global position for onTap (which lacks position info).
|
||||
Offset? _lastTapDownGlobalPosition;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
@@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
_lastTapDownGlobalPosition = d.globalPosition;
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
@@ -154,6 +157,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
@@ -171,6 +178,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
final lastPos = _lastTapDownGlobalPosition;
|
||||
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
|
||||
// Using `_lastTapDownPositionForMouseMode` instead.
|
||||
|
||||
@@ -59,7 +59,8 @@ class CanvasCoords {
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.scrollStyle =
|
||||
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
@@ -418,6 +419,74 @@ class InputModel {
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157241
|
||||
// Infer CapsLock state from the character output.
|
||||
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
|
||||
// incorrect CapsLock state on iOS.
|
||||
bool _getIosCapsFromCharacter(KeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(
|
||||
ch, HardwareKeyboard.instance.isShiftPressed);
|
||||
}
|
||||
|
||||
// RawKeyEvent version of _getIosCapsFromCharacter.
|
||||
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
|
||||
}
|
||||
|
||||
// Shared implementation for inferring CapsLock state from character.
|
||||
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
|
||||
//
|
||||
// Limitations:
|
||||
// 1. This inference assumes the client and server use the same keyboard layout.
|
||||
// If layouts differ (e.g., client uses EN, server uses DE), the character output
|
||||
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
|
||||
// layout, making it impossible to correctly infer CapsLock state from the
|
||||
// character alone.
|
||||
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
|
||||
// produces lowercase). This method cannot handle that case correctly.
|
||||
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
|
||||
if (ch == null || ch.length != 1) return false;
|
||||
// Use Dart's built-in Unicode-aware case detection
|
||||
final upper = ch.toUpperCase();
|
||||
final lower = ch.toLowerCase();
|
||||
final isUpper = upper == ch && lower != ch;
|
||||
final isLower = lower == ch && upper != ch;
|
||||
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
|
||||
if (!isUpper && !isLower) return false;
|
||||
return isUpper != shiftPressed;
|
||||
}
|
||||
|
||||
int _buildLockModes(bool iosCapsLock) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (isIOS) {
|
||||
if (iosCapsLock) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
// Ignore "NumLock/ScrollLock" on iOS for now.
|
||||
} else {
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
}
|
||||
return lockModes;
|
||||
}
|
||||
|
||||
// This function must be called after the peer info is received.
|
||||
// Because `sessionGetKeyboardMode` relies on the peer version.
|
||||
updateKeyboardMode() async {
|
||||
@@ -550,6 +619,11 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && e is RawKeyDownEvent) {
|
||||
iosCapsLock = _getIosCapsFromRawCharacter(e);
|
||||
}
|
||||
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (!e.repeat) {
|
||||
@@ -586,7 +660,7 @@ class InputModel {
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e);
|
||||
mapKeyboardModeRaw(e, iosCapsLock);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
@@ -622,6 +696,11 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
|
||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
||||
}
|
||||
|
||||
if (e is KeyUpEvent) {
|
||||
handleKeyUpEventModifiers(e);
|
||||
} else if (e is KeyDownEvent) {
|
||||
@@ -667,7 +746,8 @@ class InputModel {
|
||||
e.character ?? '',
|
||||
e.physicalKey.usbHidUsage & 0xFFFF,
|
||||
// Show repeat event be converted to "release+press" events?
|
||||
e is KeyDownEvent || e is KeyRepeatEvent);
|
||||
e is KeyDownEvent || e is KeyRepeatEvent,
|
||||
iosCapsLock);
|
||||
} else {
|
||||
legacyKeyboardMode(e);
|
||||
}
|
||||
@@ -676,23 +756,9 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// Send Key Event
|
||||
void newKeyboardMode(String character, int usbHid, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
void newKeyboardMode(
|
||||
String character, int usbHid, bool down, bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
sessionId: sessionId,
|
||||
character: character,
|
||||
@@ -701,7 +767,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -732,27 +798,14 @@ class InputModel {
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(e.character ?? '', platformCode, positionCode, down);
|
||||
inputRawKey(
|
||||
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down,
|
||||
bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
bind.sessionHandleFlutterRawKeyEvent(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
@@ -826,6 +879,9 @@ class InputModel {
|
||||
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
|
||||
bool hasStaleButtonsOnMouseUp =
|
||||
type == _kMouseEventUp && evt.buttons == _lastButtons;
|
||||
|
||||
// Check update event type and set buttons to be sent.
|
||||
int buttons = _lastButtons;
|
||||
if (type == _kMouseEventMove) {
|
||||
@@ -850,7 +906,7 @@ class InputModel {
|
||||
buttons = evt.buttons;
|
||||
}
|
||||
}
|
||||
_lastButtons = evt.buttons;
|
||||
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
|
||||
|
||||
out['buttons'] = buttons;
|
||||
out['type'] = type;
|
||||
@@ -1218,6 +1274,28 @@ class InputModel {
|
||||
_trackpadLastDelta = Offset.zero;
|
||||
}
|
||||
|
||||
// iOS Magic Mouse duplicate event detection.
|
||||
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
|
||||
// for the same click in certain areas (like top-left corner).
|
||||
int _lastMouseDownTimeMs = 0;
|
||||
ui.Offset _lastMouseDownPos = ui.Offset.zero;
|
||||
|
||||
/// Check if a touch tap event should be ignored because it's a duplicate
|
||||
/// of a recent mouse event (iOS Magic Mouse issue).
|
||||
bool shouldIgnoreTouchTap(ui.Offset pos) {
|
||||
if (!isIOS) return false;
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final dt = nowMs - _lastMouseDownTimeMs;
|
||||
final distance = (_lastMouseDownPos - pos).distance;
|
||||
// If touch tap is within 2000ms and 80px of the last mouse down,
|
||||
// it's likely a duplicate event from the same Magic Mouse click.
|
||||
if (dt >= 0 && dt < 2000 && distance < 80.0) {
|
||||
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onPointDownImage(PointerDownEvent e) {
|
||||
debugPrint("onPointDownImage ${e.kind}");
|
||||
_stopFling = true;
|
||||
@@ -1227,6 +1305,13 @@ class InputModel {
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
|
||||
// Track mouse down events for duplicate detection on iOS.
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
if (e.kind == ui.PointerDeviceKind.mouse) {
|
||||
_lastMouseDownTimeMs = nowMs;
|
||||
_lastMouseDownPos = e.position;
|
||||
}
|
||||
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
@@ -1768,9 +1853,9 @@ class InputModel {
|
||||
// Simulate a key press event.
|
||||
// `usbHidUsage` is the USB HID usage code of the key.
|
||||
Future<void> tapHidKey(int usbHidUsage) async {
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
|
||||
}
|
||||
|
||||
Future<void> onMobileVolumeUp() async =>
|
||||
|
||||
@@ -55,6 +55,10 @@ 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 {
|
||||
@@ -94,13 +98,35 @@ impl EncoderApi for HwRamEncoder {
|
||||
}
|
||||
};
|
||||
match Encoder::new(ctx.clone()) {
|
||||
Ok(encoder) => Ok(HwRamEncoder {
|
||||
encoder,
|
||||
format,
|
||||
pixfmt: ctx.pixfmt,
|
||||
bitrate,
|
||||
config,
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
}
|
||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||
}
|
||||
}
|
||||
@@ -171,6 +197,7 @@ 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,
|
||||
@@ -181,6 +208,22 @@ 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(())
|
||||
@@ -234,6 +277,43 @@ 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()),
|
||||
@@ -252,6 +332,18 @@ 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;
|
||||
@@ -264,17 +356,21 @@ 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
|
||||
}
|
||||
@@ -761,3 +857,53 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,16 +357,6 @@ static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Helper function to get display name from DisplayID
|
||||
static std::string GetDisplayName(CGDirectDisplayID displayId) {
|
||||
// Note: NSScreen.localizedName is only available on macOS 10.15+
|
||||
// Since we target 10.14, we avoid using @available checks that would
|
||||
// require __isPlatformVersionAtLeast runtime function.
|
||||
// For now, we just return "Unknown" for all displays.
|
||||
// This is only used for logging purposes, so it doesn't affect functionality.
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Helper function to find DisplayID by UUID from current online displays
|
||||
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
|
||||
uint32_t count = 0;
|
||||
@@ -404,9 +394,7 @@ static bool RestoreAllGammas() {
|
||||
const CGGammaValue* blue = green + sampleCount;
|
||||
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
|
||||
if (error != kCGErrorSuccess) {
|
||||
std::string displayName = GetDisplayName(d);
|
||||
NSLog(@"Failed to restore gamma for display (Name: %s, ID: %u, UUID: %s, error: %d)",
|
||||
displayName.c_str(), (unsigned)d, uuid.c_str(), error);
|
||||
NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
@@ -886,8 +874,7 @@ extern "C" bool MacSetPrivacyMode(bool on) {
|
||||
blackoutAttemptCount++;
|
||||
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
|
||||
if (error != kCGErrorSuccess) {
|
||||
std::string displayName = GetDisplayName(d);
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout display (Name: %s, ID: %u, UUID: %s, error: %d)", displayName.c_str(), (unsigned)d, uuid.c_str(), error);
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
|
||||
} else {
|
||||
blackoutSuccessCount++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user