diff --git a/client/FileManager.cpp b/client/FileManager.cpp index cc7ec1f..7fad869 100644 --- a/client/FileManager.cpp +++ b/client/FileManager.cpp @@ -33,7 +33,9 @@ CFileManager::CFileManager(CClientSocket *pClient, int h, void* user):CManager(p // 初始化V2文件传输模块 CKernelManager* main = (CKernelManager*)pClient->GetMain(); - InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg, + m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature; + if (!m_Signature.empty()) + InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg, main ? main->m_LoginSignature : pClient->m_LoginSignature, 64, 50, Logf); // 发送驱动器列表, 开始进行文件管理,建立新线程 @@ -48,7 +50,8 @@ CFileManager::~CFileManager() SAFE_CLOSE_HANDLE(m_hSearchThread); } m_UploadList.clear(); - UninitFileUpload(); + if (!m_Signature.empty()) + UninitFileUpload(); } diff --git a/client/FileManager.h b/client/FileManager.h index 5643a73..958818f 100644 --- a/client/FileManager.h +++ b/client/FileManager.h @@ -35,6 +35,7 @@ private: UINT m_nTransferMode; char m_strCurrentProcessFileName[MAX_PATH]; // 当前正在处理的文件 __int64 m_nCurrentProcessFileLength; // 当前正在处理的文件的长度 + std::string m_Signature; bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath); bool UploadToRemote(LPBYTE lpBuffer); void UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize); diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index f30ef41..a085400 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -100,7 +100,8 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL extern ClientApp g_MyApp; SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID CKernelManager* main = (CKernelManager*)ClientObject->GetMain(); - InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg, + m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature; + if (!m_Signature.empty()) InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg, main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf); #endif m_isGDI = TRUE; @@ -718,7 +719,8 @@ VOID CScreenManager::SendBitMapInfo() CScreenManager::~CScreenManager() { Mprintf("ScreenManager 析构函数\n"); - UninitFileUpload(); + if (!m_Signature.empty()) + UninitFileUpload(); m_bIsWorking = FALSE; m_bAudioThreadRunning = FALSE; // 停止音频线程 diff --git a/client/ScreenManager.h b/client/ScreenManager.h index 14fda1b..c0d8c67 100644 --- a/client/ScreenManager.h +++ b/client/ScreenManager.h @@ -90,7 +90,7 @@ public: uint64_t m_nReconnectTime = 0; // 重连开始时间 uint64_t m_DlgID = 0; BOOL m_SendFirst = FALSE; - + std::string m_Signature; // 虚拟桌面 BOOL m_virtual; POINT m_point; diff --git a/linux/README.md b/linux/README.md index 89b85d2..9e59134 100644 --- a/linux/README.md +++ b/linux/README.md @@ -261,7 +261,7 @@ loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type | `InstallTime` | 首次安装时间 | `1709856000` | | `PublicIP` | 公网 IP 缓存 | `1.2.3.4` | | `GeoLocation` | 地理位置缓存 | `北京市` | -| `QualityLevel` | 屏幕质量等级 | `-1` (自适应) | +| `QualityLevel` | 屏幕质量等级 | `2` (Good / H264 1080P) | ### 质量等级说明 diff --git a/linux/ScreenHandler.h b/linux/ScreenHandler.h index 3847a4c..f60f4a3 100644 --- a/linux/ScreenHandler.h +++ b/linux/ScreenHandler.h @@ -911,7 +911,14 @@ public: // 加载保存的质量设置 void LoadQualitySettings() { - m_qualityLevel = (int8_t)m_config.GetInt("QualityLevel", QUALITY_ADAPTIVE); + // Default to QUALITY_GOOD (H264 1080P) — same as Windows ScreenManager.cpp + // and the explicit hardcoded default in macos/ScreenHandler.mm. Using + // QUALITY_ADAPTIVE here told the controller's EvaluateQuality to auto- + // upgrade to Ultra/High (DIFF/RGB565), breaking web sessions whose + // browser decoder only handles H264. Web sessions also get a clamp on + // the server side as defense in depth; this default change ensures + // Linux clients don't request adaptive in the first place. + m_qualityLevel = (int8_t)m_config.GetInt("QualityLevel", QUALITY_GOOD); Mprintf(">>> LoadQualitySettings: level=%d\n", m_qualityLevel); // 如果有保存的具体等级,立即应用 diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index 0953abe..f640fdb 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -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; diff --git a/server/web/index.html b/server/web/index.html index e88a887..fa947c7 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -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;