Fix: Web remote desktop reliability and UX

- Server: clamp web session adaptive quality to H264-only levels (>=Good) in EvaluateQuality and ApplyQualityLevel; Ultra/High (DIFF/RGB565) caused the browser to freeze ~1 min into a session
- Server: move session-type detection to the top of ScreenSpyDlg::OnInitDialog and skip SetWindowPlacement/EnterFullScreen for hidden web sessions, eliminating the MFC dialog flash on web-triggered opens
- Linux client: default QualityLevel from QUALITY_ADAPTIVE to QUALITY_GOOD to match Windows/macOS so the server's adaptive controller doesn't auto-upgrade to non-H264 algorithms
- Web: clear the floating quick-action toolbar on fullscreen exit so its row of buttons (RDP reset / Mouse / Close) doesn't stay pinned to the top of the page
- Web: route F11 to the remote in control mode instead of toggling local fullscreen
- Web: route Esc to the remote in control mode via the Keyboard Lock API instead of exiting native fullscreen
This commit is contained in:
yuanyuanxiang
2026-05-19 18:06:41 +02:00
parent d757c33bcb
commit cd43caafb2
8 changed files with 135 additions and 46 deletions

View File

@@ -567,6 +567,26 @@ BOOL CScreenSpyDlg::OnInitDialog()
__super::OnInitDialog();
SetIcon(m_hIcon,FALSE);
// Determine session type FIRST so the window-show machinery below
// (SetWindowPlacement with showCmd=SW_MAXIMIZE, EnterFullScreen) can be
// skipped for hidden web sessions. Without this, the dialog briefly
// flashes on-screen — the hide used to happen ~200 lines later, after
// a chain of init work that the user could see in the meantime.
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
if (isMfcSession) {
// MFC-triggered: clear the flag, don't register with WebService.
WebService().ClearMfcTriggered(m_ClientID);
// m_bIsWebSession remains false (default)
} else if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
// Web-triggered: register and hide upfront.
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
m_bHide = true;
m_bIsWebSession = true;
ShowWindow(SW_HIDE);
}
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
m_ClientID, isMfcSession ? 1 : 0, m_bIsWebSession.load() ? 1 : 0);
// 获取默认 IME 上下文ImmAssociateContext 返回之前关联的上下文)
// 先禁用再恢复,以获取原始上下文句柄
m_hOldIMC = ImmAssociateContext(m_hWnd, NULL);
@@ -730,11 +750,19 @@ BOOL CScreenSpyDlg::OnInitDialog()
wp.rcNormalPosition.right = normalX + normalWidth;
wp.rcNormalPosition.bottom = normalY + normalHeight;
wp.showCmd = SW_MAXIMIZE;
SetWindowPlacement(&wp);
// 同时初始化 m_struOldWndpl供全屏退出时使用
m_struOldWndpl = wp;
m_Settings.FullScreen ? EnterFullScreen() : LeaveFullScreen();
// Skip the placement + fullscreen machinery for hidden web sessions —
// those calls would briefly flash the dialog on the user's desktop
// before the early ShowWindow(SW_HIDE) above takes effect at WM_PAINT
// time. m_struOldWndpl still gets a sane snapshot so any defensive
// LeaveFullScreen path later has something to restore against.
if (!m_bIsWebSession) {
SetWindowPlacement(&wp);
m_struOldWndpl = wp;
m_Settings.FullScreen ? EnterFullScreen() : LeaveFullScreen();
} else {
m_struOldWndpl = wp;
}
// 启动传输速率更新定时器 (1秒)
SetTimer(4, 1000, NULL);
@@ -771,32 +799,9 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
// Determine session type: MFC or Web
// Must check MfcTriggered FIRST - if MFC triggered this dialog, it's NOT a web session
// even if WebTriggered is also true (happens when Web is already open for same device)
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
bool isWebSession = false;
if (isMfcSession) {
// MFC session: clear the flag, don't register with WebService
WebService().ClearMfcTriggered(m_ClientID);
// m_bIsWebSession remains false (default)
} else {
// Check if this is a Web session
isWebSession = WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions();
// Only register screen context for Web sessions
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
// This prevents MFC close from deleting Web's context (they share same device_id key)
if (isWebSession) {
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
m_bHide = true;
m_bIsWebSession = true;
ShowWindow(SW_HIDE);
}
}
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
m_ClientID, isMfcSession ? 1 : 0, isWebSession ? 1 : 0);
// Session type detection and visibility moved to the top of this function
// (right after __super::OnInitDialog) so SetWindowPlacement / EnterFullScreen
// above can be skipped for web sessions and avoid a visible flash.
return TRUE;
}
@@ -2277,6 +2282,16 @@ void CScreenSpyDlg::EvaluateQuality()
// 2. 计算目标等级
int targetLevel = GetTargetQualityLevel(rtt, m_bUsingFRP);
// Web 会话只解码 H264。levels 0 (Ultra/DIFF) 和 1 (High/RGB565) 在网络好的时候
// 会被自适应控制器选中,但 device 切到那两个算法后浏览器的 WebCodecs decoder
// 就吃不下了,画面冻在最后一个 H264 帧上 (用户感受是 ~1 分钟后卡死,
// 因为 EvaluateQuality 启动 1 分钟才开始评估)。Clamp 到最高质量的 H264 等级
// 让自适应在 H264 区间内继续工作.
if (m_bIsWebSession && targetLevel < QUALITY_GOOD) {
targetLevel = QUALITY_GOOD;
}
int currentLevel = m_AdaptiveQuality.currentLevel;
if (targetLevel == currentLevel) {
@@ -2322,6 +2337,14 @@ void CScreenSpyDlg::ApplyQualityLevel(int level, bool persist)
{
if (level < 0 || level >= QUALITY_COUNT) return;
// Defense in depth: ALL paths that set quality level (adaptive controller,
// manual menu pick, restore from config, …) flow through here. Web
// sessions cannot use non-H264 levels — see EvaluateQuality for the
// failure mode explanation.
if (m_bIsWebSession && level < QUALITY_GOOD) {
level = QUALITY_GOOD;
}
const QualityProfile& profile = GetQualityProfile(level);
int oldMaxWidth = m_AdaptiveQuality.currentMaxWidth;
int newMaxWidth = profile.maxWidth;

View File

@@ -2290,10 +2290,14 @@
});
document.addEventListener('keydown', function(e) {
if (e.key === 'F11' && document.getElementById('screen-page').classList.contains('active')) {
e.preventDefault();
toggleFullscreen();
}
if (e.key !== 'F11') return;
if (!document.getElementById('screen-page').classList.contains('active')) return;
// In control mode F11 is a remote keystroke — let it fall through
// to the desktop key handler (which calls sendKey). Outside control
// mode F11 toggles our page fullscreen as the local convenience.
if (controlEnabled) return;
e.preventDefault();
toggleFullscreen();
});
// Control mode state (mouse/keyboard control)
@@ -2365,6 +2369,40 @@
document.getElementById('screen-page')?.classList.contains('pseudo-fullscreen'));
}
// Keyboard Lock keeps F11 / Esc from triggering their browser-default
// behaviour (F11 → toggle browser fullscreen, Esc → exit native page
// fullscreen) while we're actively controlling a remote desktop —
// those keys belong to the remote OS in that mode, not the local
// browser. Lock is no-op on browsers without the API.
function updateKeyboardLock() {
if (!navigator.keyboard || !navigator.keyboard.lock) return;
if (isFullscreen() && controlEnabled) {
navigator.keyboard.lock(['Escape', 'F11']).catch(() => {});
} else {
navigator.keyboard.unlock();
}
}
// Unified fullscreen-change handler. Always clears the floating
// quick-action toolbar (its three buttons duplicate the top-right
// toolbar that becomes visible again on exit — leaving them pinned
// to the top of the page is just noise). Then re-evaluates the
// keyboard lock and, for touch devices, refreshes the orientation-
// dependent layout.
function onFullscreenChange() {
if (!isFullscreen()) {
const fb = document.getElementById('floating-toolbar');
if (fb) fb.classList.remove('visible');
toolbarVisible = false;
if (toolbarHideTimer) {
clearTimeout(toolbarHideTimer);
toolbarHideTimer = null;
}
}
updateKeyboardLock();
if (isTouchDevice) updateUIForOrientation();
}
function toggleFloatingToolbar() {
const toolbar = document.getElementById('floating-toolbar');
toolbarVisible = !toolbarVisible;
@@ -2517,6 +2555,12 @@
if (controlEnabled) {
applyRemoteCursor(currentCursorIndex);
}
// Sync the keyboard lock: in fullscreen + control mode, ESC and F11
// must be passed through to the remote OS instead of triggering the
// browser's exit-fullscreen / toggle-fullscreen behavior. The
// updateKeyboardLock helper no-ops if either condition is missing
// or the API is unavailable.
updateKeyboardLock();
}
// Update cursor overlay position (accounting for zoom/pan transform)
@@ -3279,7 +3323,10 @@
document.addEventListener('keydown', function(e) {
if (!document.getElementById('screen-page').classList.contains('active')) return;
if (e.target.tagName === 'INPUT') return;
if (e.key === 'F11') return;
// F11 belongs to the local fullscreen toggle UNLESS we're actively
// controlling the remote — see the F11 handler above. Skip here
// to let that one handle it.
if (e.key === 'F11' && !controlEnabled) return;
e.preventDefault();
sendKey(e.keyCode, true, e.altKey);
});
@@ -3287,7 +3334,7 @@
document.addEventListener('keyup', function(e) {
if (!document.getElementById('screen-page').classList.contains('active')) return;
if (e.target.tagName === 'INPUT') return;
if (e.key === 'F11') return;
if (e.key === 'F11' && !controlEnabled) return;
e.preventDefault();
sendKey(e.keyCode, false, e.altKey);
});
@@ -3480,9 +3527,10 @@
resizeTimer = setTimeout(updateUIForOrientation, 100);
});
// Listen for fullscreen changes
document.addEventListener('fullscreenchange', updateUIForOrientation);
document.addEventListener('webkitfullscreenchange', updateUIForOrientation);
// Touch-device-only handlers continue here; the unified
// fullscreen change handler (onFullscreenChange) is registered
// below outside this branch so desktop browsers also get the
// floating-toolbar cleanup + keyboard lock sync.
// Input shortcut buttons event handlers
// Shortcuts only visible when keyboard is open, so no extra check needed
@@ -3528,6 +3576,11 @@
if (btnKeyboard) btnKeyboard.classList.add('ui-hidden');
if (btnKeyboardBar) btnKeyboardBar.classList.add('ui-hidden');
}
// Unified fullscreen change handler — registered for all devices.
// Touch devices need updateUIForOrientation; desktop devices need
// the floating-toolbar cleanup. Both need the keyboard lock sync.
document.addEventListener('fullscreenchange', onFullscreenChange);
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
// Keyboard buttons: capture focus state and prevent blur from updating button state
function bindKeyboardBtnEvents(btn) {
if (!btn) return;