Compare commits

...

10 Commits

20 changed files with 1262 additions and 367 deletions
+26 -2
View File
@@ -250,10 +250,34 @@ jobs:
- name: Package Portable
shell: pwsh
run: |
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
$portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
Copy-Item "${{ github.workspace }}\build\windows\x64\release\*.dll" $portableDir -Force
$portableFiles = @(
@("crossdesk.exe", "CrossDesk.exe"),
@("crossdesk_service.exe", "crossdesk_service.exe"),
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
)
foreach ($file in $portableFiles) {
$source = Join-Path $buildDir $file[0]
$destination = Join-Path $portableDir $file[1]
if (!(Test-Path $source)) {
throw "Missing portable package file: $source"
}
Copy-Item $source $destination -Force
}
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
foreach ($file in $portableFiles) {
$packagedFile = Join-Path $portableDir $file[1]
if (!(Test-Path $packagedFile)) {
throw "Portable package is missing: $packagedFile"
}
}
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact
+23
View File
@@ -53,6 +53,29 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### Windows 服务(CrossDesk Service
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
- 支持从控制端发送 `Ctrl+Alt+Del`SAS);
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
Windows 安装包会自动打包 `crossdesk_service.exe``crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe``crossdesk_service.exe``crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell
```
# 安装
.\CrossDesk.exe --service-install
# 启动
.\CrossDesk.exe --service-start
# 查看状态
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-stop
# 卸载
.\CrossDesk.exe --service-uninstall
```
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
## 如何编译
依赖:
+25
View File
@@ -56,6 +56,31 @@ Enter the **Remote Device ID** and **Password**, then click Connect to access th
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### Windows Service (CrossDesk Service)
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
- Remote `Ctrl+Alt+Del` (SAS) delivery.
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
```
# install
.\CrossDesk.exe --service-install
# start
.\CrossDesk.exe --service-start
# check status
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-ping
# stop
.\CrossDesk.exe --service-stop
# uninstall
.\CrossDesk.exe --service-uninstall
```
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
## How to build
Requirements:
+170 -149
View File
@@ -19,155 +19,176 @@ struct TranslationRow {
};
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制", "Control", u8"Управление") \
X(release_mouse, u8"释放", "Release", u8"Освободить") \
X(audio_capture, u8"声音", "Audio", u8"Звук") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", \
u8"Активен защищенный рабочий стол") \
X(remote_service_unavailable, u8"远端Windows服务不可用", \
"Remote Windows service unavailable", \
u8"Служба Windows на удаленной стороне недоступна") \
X(remote_unlock_requires_secure_desktop, \
u8"当前仍需要安全桌面专用采集/输入", \
"Secure desktop capture/input is still required", \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего стола") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"高", "High", u8"Высокое") \
X(video_quality_medium, u8"", "Medium", u8"Среднее") \
X(video_quality_low, u8"低", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
u8"Свернуть панель управления") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
X(remote_service_unavailable, u8"远端Windows服务不可用", \
"Remote Windows service unavailable", \
u8"Служба Windows на удаленной стороне недоступна") \
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
X(windows_service_setup_message, \
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \
X(install_windows_service, u8"安装", "Install", \
u8"Установить службу") \
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
u8"Установка службы...") \
X(windows_service_install_success, u8"服务已安装并启动", \
"Service installed and started", u8"Служба установлена и запущена") \
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \
X(remote_unlock_requires_secure_desktop, \
u8"当前仍需要安全桌面专用采集/输入", \
"Secure desktop capture/input is still required", \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
u8"стола") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"高", "High", u8"Высокое") \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
X(video_quality_low, u8"", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件发送", "Select File to Send", \
u8"Выбрать файл для отправки") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
+37 -4
View File
@@ -42,6 +42,8 @@
namespace crossdesk {
namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
const ImWchar* GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) {
@@ -651,18 +653,37 @@ int Render::ScreenCapturerInit() {
fps,
[this, fps](unsigned char* data, int size, int width, int height,
const char* display_name) -> void {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
const auto now_time =
static_cast<uint64_t>(std::chrono::duration_cast<
std::chrono::milliseconds>(
std::chrono::steady_clock::now()
.time_since_epoch())
.count());
auto duration = now_time - last_frame_time_;
if (duration * fps >= 1000) { // ~60 FPS
const std::string stream_id = display_name ? display_name : "";
const bool resumed_after_gap =
last_frame_time_ != 0 &&
duration >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed =
!last_video_frame_stream_id_.empty() &&
last_video_frame_stream_id_ != stream_id;
if (resumed_after_gap || stream_changed) {
if (RequestVideoKeyFrame(peer_, stream_id.c_str()) == 0) {
LOG_INFO(
"Request video key frame before sending captured frame, "
"stream='{}', gap_ms={}, stream_changed={}",
stream_id, duration, stream_changed);
}
}
XVideoFrame frame;
frame.data = (const char*)data;
frame.size = size;
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(peer_);
SendVideoFrame(peer_, &frame, display_name);
SendVideoFrame(peer_, &frame, stream_id.c_str());
last_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time;
}
});
@@ -1549,6 +1570,10 @@ int Render::DrawMainWindow() {
UpdateNotificationWindow();
#if _WIN32 && CROSSDESK_PORTABLE
PortableServiceInstallWindow();
#endif
#ifdef __APPLE__
if (show_request_permission_window_) {
RequestPermissionWindow();
@@ -1711,6 +1736,10 @@ int Render::Run() {
InitializeModules();
InitializeMainWindow();
#if _WIN32 && CROSSDESK_PORTABLE
CheckPortableWindowsService();
#endif
const int scaled_video_width_ = 160;
const int scaled_video_height_ = 90;
@@ -2226,6 +2255,10 @@ void Render::Cleanup() {
CleanupFactories();
CleanupPeers();
#if _WIN32 && CROSSDESK_PORTABLE
JoinPortableWindowsServiceInstallThread();
#endif
WaitForThumbnailSaveTasks();
AudioDeviceDestroy();
+21
View File
@@ -382,6 +382,19 @@ class Render {
void HandleWindowsServiceIntegration();
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#if CROSSDESK_PORTABLE
enum class PortableServiceInstallState {
idle,
installing,
succeeded,
failed,
};
void CheckPortableWindowsService();
int PortableServiceInstallWindow();
void StartPortableWindowsServiceInstall();
void JoinPortableWindowsServiceInstallThread();
#endif
#endif
private:
@@ -548,6 +561,13 @@ class Render {
uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_tick_ = 0;
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
#if CROSSDESK_PORTABLE
bool portable_service_prompt_checked_ = false;
bool show_portable_service_install_window_ = false;
std::atomic<PortableServiceInstallState> portable_service_install_state_{
PortableServiceInstallState::idle};
std::thread portable_service_install_thread_;
#endif
#endif
// stream window render
@@ -672,6 +692,7 @@ class Render {
KeyboardCapturer* keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
uint64_t last_frame_time_;
std::string last_video_frame_stream_id_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
+61 -22
View File
@@ -15,6 +15,22 @@
namespace crossdesk {
namespace {
void ShowControlBarTooltip(const std::string& text) {
if (!ImGui::IsItemHovered() || text.empty()) {
return;
}
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
} // namespace
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
@@ -162,6 +178,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2 btn_min = ImGui::GetItemRectMin();
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
ShowControlBarTooltip(
localization::select_display[localization_language_index_]);
props->display_selectable_hovered_ = false;
if (ImGui::BeginPopup("display")) {
@@ -185,12 +203,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::EndPopup();
}
ImGui::SetWindowFontScale(0.5f);
ImGui::SetWindowFontScale(0.35f);
ImVec2 text_size = ImGui::CalcTextSize(
std::to_string(props->selected_display_ + 1).c_str());
ImVec2 text_pos =
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f,
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f);
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
ImGui::GetWindowDrawList()->AddText(
text_pos, IM_COL32(0, 0, 0, 255),
std::to_string(props->selected_display_ + 1).c_str());
@@ -218,25 +236,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
ImGui::OpenPopup("shortcut");
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text(
"%s",
localization::send_shortcut[localization_language_index_].c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ShowControlBarTooltip(
localization::send_shortcut[localization_language_index_]);
props->shortcut_selectable_hovered_ = false;
if (ImGui::BeginPopup("shortcut")) {
ImGui::SetWindowFontScale(0.5f);
std::string sas_label =
"Ctrl+Alt+Del - " +
localization::send_sas[localization_language_index_];
std::string lock_label =
"Win+L - " + localization::lock_remote[localization_language_index_];
std::string sas_label = "Ctrl+Alt+Del";
std::string lock_label = "Win+L";
if (ImGui::Selectable(sas_label.c_str())) {
send_service_command(ServiceCommandFlag::send_sas, "SAS");
}
@@ -268,6 +275,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: localization::control_mouse[localization_language_index_];
}
}
const bool mouse_button_hovered = ImGui::IsItemHovered();
const std::string mouse_tooltip =
props->enable_mouse_control_
? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_];
ShowControlBarTooltip(mouse_tooltip);
if (!props->enable_mouse_control_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
@@ -280,8 +293,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2(
mouse_x + button_width - line_padding - line_thickness * 0.7f,
mouse_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
line_thickness);
}
@@ -313,6 +326,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
props->control_data_label_.c_str());
}
}
const bool audio_button_hovered = ImGui::IsItemHovered();
const std::string audio_tooltip =
props->audio_capture_button_pressed_
? localization::mute[localization_language_index_]
: localization::audio_capture[localization_language_index_];
ShowControlBarTooltip(audio_tooltip);
if (!props->audio_capture_button_pressed_) {
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
@@ -325,8 +344,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2(
audio_x + button_width - line_padding - line_thickness * 0.7f,
audio_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
line_thickness);
}
@@ -339,6 +358,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string path = OpenFileDialog(title);
ProcessSelectedFile(path, props, file_label_);
}
ShowControlBarTooltip(
localization::select_file[localization_language_index_]);
ImGui::SameLine();
// net traffic stats button
@@ -363,6 +384,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: localization::show_net_traffic_stats
[localization_language_index_];
}
const std::string net_traffic_stats_tooltip =
props->net_traffic_stats_button_pressed_
? localization::hide_net_traffic_stats[localization_language_index_]
: localization::show_net_traffic_stats
[localization_language_index_];
ShowControlBarTooltip(net_traffic_stats_tooltip);
if (button_color_style_pushed) {
ImGui::PopStyleColor();
@@ -389,6 +416,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
}
props->reset_control_bar_pos_ = true;
}
const std::string fullscreen_tooltip =
fullscreen_button_pressed_
? localization::exit_fullscreen[localization_language_index_]
: localization::fullscreen[localization_language_index_];
ShowControlBarTooltip(fullscreen_tooltip);
ImGui::SameLine();
// close button
@@ -398,6 +430,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2(button_width, button_height))) {
CleanupPeer(props);
}
ShowControlBarTooltip(
localization::disconnect[localization_language_index_]);
ImGui::SameLine();
@@ -427,6 +461,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: ICON_FA_ANGLE_RIGHT)
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
: ICON_FA_ANGLE_LEFT);
const std::string control_bar_tooltip =
props->control_bar_expand_
? localization::collapse_control_bar[localization_language_index_]
: localization::expand_control_bar[localization_language_index_];
if (ImGui::Button(control_bar.c_str(),
ImVec2(button_height * 0.6f, button_height))) {
props->control_bar_expand_ = !props->control_bar_expand_;
@@ -438,6 +476,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
props->net_traffic_stats_button_pressed_ = false;
}
}
ShowControlBarTooltip(control_bar_tooltip);
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
NetTrafficStats(props);
@@ -0,0 +1,279 @@
#include "render.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include <shellapi.h>
#include <vector>
#include "localization.h"
#include "rd_log.h"
#include "service_host.h"
namespace crossdesk {
namespace {
std::filesystem::path GetCurrentExecutablePath() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
DWORD length =
GetModuleFileNameW(nullptr, buffer.data(),
static_cast<DWORD>(buffer.size()));
if (length == 0) {
return {};
}
if (length < buffer.size()) {
return std::filesystem::path(buffer.data(), buffer.data() + length);
}
if (buffer.size() >= 32768) {
return {};
}
buffer.resize(buffer.size() * 2);
}
}
bool InstallServiceWithElevation() {
const std::filesystem::path executable_path = GetCurrentExecutablePath();
if (executable_path.empty()) {
LOG_ERROR("Portable service install failed: current executable not found");
return false;
}
const std::filesystem::path service_path =
executable_path.parent_path() / L"crossdesk_service.exe";
const std::filesystem::path helper_path =
executable_path.parent_path() / L"crossdesk_session_helper.exe";
if (!std::filesystem::exists(service_path) ||
!std::filesystem::exists(helper_path)) {
LOG_ERROR(
"Portable service install failed: service binaries missing, service={}, "
"helper={}",
service_path.string(), helper_path.string());
return false;
}
std::wstring executable = executable_path.wstring();
std::wstring working_dir = executable_path.parent_path().wstring();
std::wstring parameters = L"--service-install";
SHELLEXECUTEINFOW execute_info{};
execute_info.cbSize = sizeof(execute_info);
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
execute_info.hwnd = nullptr;
execute_info.lpVerb = L"runas";
execute_info.lpFile = executable.c_str();
execute_info.lpParameters = parameters.c_str();
execute_info.lpDirectory = working_dir.c_str();
execute_info.nShow = SW_HIDE;
if (!ShellExecuteExW(&execute_info)) {
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
GetLastError());
return false;
}
DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE);
DWORD exit_code = 1;
if (wait_result == WAIT_OBJECT_0) {
GetExitCodeProcess(execute_info.hProcess, &exit_code);
} else {
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
}
CloseHandle(execute_info.hProcess);
if (exit_code != 0) {
LOG_ERROR("Portable service install command failed, exit_code={}",
exit_code);
return false;
}
const bool started = StartCrossDeskService();
if (!started) {
LOG_WARN("Portable service installed but start failed");
}
return IsCrossDeskServiceInstalled() && started;
}
} // namespace
void Render::CheckPortableWindowsService() {
if (portable_service_prompt_checked_) {
return;
}
portable_service_prompt_checked_ = true;
if (IsCrossDeskServiceInstalled()) {
return;
}
portable_service_install_state_.store(PortableServiceInstallState::idle,
std::memory_order_relaxed);
show_portable_service_install_window_ = true;
}
void Render::StartPortableWindowsServiceInstall() {
PortableServiceInstallState expected = PortableServiceInstallState::idle;
if (!portable_service_install_state_.compare_exchange_strong(
expected, PortableServiceInstallState::installing,
std::memory_order_acq_rel)) {
if (expected != PortableServiceInstallState::failed) {
return;
}
portable_service_install_state_.store(
PortableServiceInstallState::installing, std::memory_order_release);
}
JoinPortableWindowsServiceInstallThread();
portable_service_install_thread_ = std::thread([this]() {
const bool installed = InstallServiceWithElevation();
portable_service_install_state_.store(
installed ? PortableServiceInstallState::succeeded
: PortableServiceInstallState::failed,
std::memory_order_release);
});
}
void Render::JoinPortableWindowsServiceInstallThread() {
if (portable_service_install_thread_.joinable()) {
portable_service_install_thread_.join();
}
}
int Render::PortableServiceInstallWindow() {
if (!show_portable_service_install_window_) {
return 0;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
const float window_width = title_bar_button_width_ * 12.0f;
const float window_height = title_bar_button_width_ * 4.0f;
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) /
2.0f,
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) /
2.0f),
ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(
localization::windows_service_setup_title[localization_language_index_]
.c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoTitleBar);
ImGui::Spacing();
ImGui::SetWindowFontScale(0.55f);
ImGui::SetCursorPosX(window_width * 0.08f);
ImGui::Text(
"%s",
localization::windows_service_setup_title[localization_language_index_]
.c_str());
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
const char* status_text = nullptr;
if (state == PortableServiceInstallState::installing ||
state == PortableServiceInstallState::succeeded ||
state == PortableServiceInstallState::failed) {
status_text =
localization::installing_windows_service[localization_language_index_]
.c_str();
if (state == PortableServiceInstallState::succeeded) {
status_text =
localization::windows_service_install_success
[localization_language_index_]
.c_str();
} else if (state == PortableServiceInstallState::failed) {
status_text =
localization::windows_service_install_failed
[localization_language_index_]
.c_str();
}
}
ImGui::SetWindowFontScale(0.45f);
ImGui::SetCursorPosX(window_width * 0.04f);
ImGui::SetCursorPosY(window_height * 0.22f);
ImGui::BeginChild("PortableServiceInstallContent",
ImVec2(window_width * 0.92f, window_height * 0.5f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f);
const float wrap_pos = ImGui::GetContentRegionAvail().x;
ImGui::PushTextWrapPos(wrap_pos);
ImGui::TextWrapped(
"%s",
localization::windows_service_setup_message[localization_language_index_]
.c_str());
if (status_text != nullptr) {
ImGui::Spacing();
ImGui::TextWrapped("%s", status_text);
}
ImGui::PopTextWrapPos();
ImGui::EndChild();
ImGui::SetWindowFontScale(0.5f);
const float button_y = window_height * 0.76f;
const ImGuiStyle& style = ImGui::GetStyle();
const auto default_button_width = [&style](const std::string& label) {
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
};
const std::string install_label =
localization::install_windows_service[localization_language_index_];
const std::string cancel_label =
localization::cancel[localization_language_index_];
const std::string ok_label = localization::ok[localization_language_index_];
const float buttons_width = state == PortableServiceInstallState::succeeded
? default_button_width(ok_label)
: default_button_width(install_label) +
style.ItemSpacing.x +
default_button_width(cancel_label);
ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f);
ImGui::SetCursorPosY(button_y);
if (state == PortableServiceInstallState::succeeded) {
if (ImGui::Button(ok_label.c_str())) {
show_portable_service_install_window_ = false;
JoinPortableWindowsServiceInstallThread();
}
} else {
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(install_label.c_str())) {
StartPortableWindowsServiceInstall();
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
}
ImGui::SameLine();
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(cancel_label.c_str())) {
show_portable_service_install_window_ = false;
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return 0;
}
} // namespace crossdesk
#endif
@@ -244,6 +244,31 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
return true;
}
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
ReleaseDuplication();
int current_monitor = monitor_index_.load();
if (CreateDuplicationForMonitor(current_monitor)) {
return true;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found while recreating duplication");
return false;
}
if (current_monitor < 0 ||
current_monitor >= static_cast<int>(display_info_list_.size())) {
current_monitor = 0;
monitor_index_ = 0;
}
if (CreateDuplicationForMonitor(current_monitor)) {
LOG_INFO("DXGI: recreated duplication for monitor {}",
monitor_index_.load());
return true;
}
return false;
}
void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset();
if (duplication_) {
@@ -254,6 +279,8 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
auto last_duplication_retry =
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
@@ -261,6 +288,11 @@ void ScreenCapturerDxgi::CaptureLoop() {
}
if (!duplication_) {
const auto now = std::chrono::steady_clock::now();
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
last_duplication_retry = now;
RecreateDuplicationForCurrentMonitor();
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
@@ -274,9 +306,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
}
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
// attempt to recreate duplication
ReleaseDuplication();
CreateDuplicationForMonitor(monitor_index_);
RecreateDuplicationForCurrentMonitor();
continue;
}
@@ -353,4 +383,4 @@ void ScreenCapturerDxgi::CaptureLoop() {
}
}
} // namespace crossdesk
} // namespace crossdesk
@@ -50,6 +50,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
bool InitializeDxgi();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index);
bool RecreateDuplicationForCurrentMonitor();
void CaptureLoop();
void ReleaseDuplication();
@@ -78,4 +79,4 @@ class ScreenCapturerDxgi : public ScreenCapturer {
};
} // namespace crossdesk
#endif
#endif
@@ -100,8 +100,7 @@ bool ScreenCapturerWgc::IsWgcSupported() {
}
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
int error = 0;
if (inited_ == true) return error;
if (inited_ == true) return 0;
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
@@ -112,8 +111,18 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported");
error = 2;
return error;
return 2;
}
return RebuildSessions(monitor_index_);
}
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
CleanUp();
if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported");
return 2;
}
monitor_ = GetPrimaryMonitor();
@@ -125,6 +134,13 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
return -1;
}
if (preferred_monitor_index < 0 ||
preferred_monitor_index >= static_cast<int>(display_info_list_.size())) {
preferred_monitor_index = 0;
}
monitor_index_ = preferred_monitor_index;
int error = 0;
for (int i = 0; i < display_info_list_.size(); i++) {
const auto& display = display_info_list_[i];
LOG_INFO(
@@ -138,20 +154,28 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
sessions_.back().session_->RegisterObserver(this);
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
if (error != 0) {
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
CleanUp();
return error;
}
sessions_[i].inited_ = true;
inited_ = true;
}
LOG_INFO("Default on monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
initial_monitor_index_ = monitor_index_;
if (initial_monitor_index_ < 0 ||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
initial_monitor_index_ = monitor_index_;
}
inited_ = true;
return 0;
}
int ScreenCapturerWgc::Destroy() { return 0; }
int ScreenCapturerWgc::Destroy() {
CleanUp();
return 0;
}
int ScreenCapturerWgc::Start(bool show_cursor) {
if (running_ == true) {
@@ -160,13 +184,37 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
}
if (inited_ == false) {
LOG_ERROR("Screen capturer not inited");
return 4;
const int ret = RebuildSessions(monitor_index_);
if (ret != 0) {
LOG_ERROR("Screen capturer not inited");
return ret;
}
}
int ret = StartSessions(show_cursor);
if (ret == 0) {
return 0;
}
LOG_WARN("WGC: start failed, rebuilding sessions");
ret = RebuildSessions(monitor_index_);
if (ret != 0) {
return ret;
}
return StartSessions(show_cursor);
}
int ScreenCapturerWgc::StartSessions(bool show_cursor) {
bool any_started = false;
bool active_started = false;
int last_error = 0;
for (int i = 0; i < sessions_.size(); i++) {
int active_monitor = monitor_index_;
if (active_monitor < 0 ||
active_monitor >= static_cast<int>(sessions_.size())) {
active_monitor = 0;
monitor_index_ = 0;
}
for (int i = 0; i < static_cast<int>(sessions_.size()); i++) {
if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i);
continue;
@@ -182,16 +230,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
continue;
}
if (i != 0) {
if (i != active_monitor) {
sessions_[i].session_->Pause();
sessions_[i].paused_ = true;
} else {
sessions_[i].session_->Resume();
sessions_[i].paused_ = false;
}
sessions_[i].running_ = true;
any_started = true;
if (i == active_monitor) {
active_started = true;
}
}
running_ = running_ || any_started;
}
running_ = active_started;
if (!active_started) {
LOG_ERROR("WGC: active session did not start successfully");
Stop();
return last_error != 0 ? last_error : -1;
}
if (!any_started) {
LOG_ERROR("WGC: no session started successfully");
return last_error != 0 ? last_error : -1;
@@ -349,13 +408,16 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
}
void ScreenCapturerWgc::CleanUp() {
if (inited_) {
for (auto& session : sessions_) {
if (session.session_) {
session.session_->Stop();
}
running_ = false;
for (auto& session : sessions_) {
if (session.session_) {
session.session_->Stop();
}
sessions_.clear();
}
sessions_.clear();
display_info_list_.clear();
gs_display_list.clear();
monitor_ = nullptr;
inited_ = false;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -40,6 +40,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
protected:
void CleanUp();
int RebuildSessions(int preferred_monitor_index);
int StartSessions(bool show_cursor);
private:
HMONITOR monitor_;
@@ -74,4 +76,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::mutex frame_mutex_;
};
} // namespace crossdesk
#endif
#endif
@@ -33,6 +33,8 @@ constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000;
constexpr DWORD kPostSecureDesktopRestartRetryMs = 500;
constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000;
constexpr int kSecureDesktopCaptureMinFps = 30;
constexpr int kSecureDesktopCaptureMaxIntervalMs =
1000 / kSecureDesktopCaptureMinFps;
@@ -392,21 +394,46 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
return;
}
const char* raw_display_name = display_name ? display_name : "";
std::string mapped_name;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(display_name);
auto it = label_alias_.find(raw_display_name);
if (it != label_alias_.end())
mapped_name = it->second;
else
mapped_name = display_name;
mapped_name = raw_display_name;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
if (post_secure_desktop_waiting_for_frame_.load(
std::memory_order_relaxed) &&
!post_secure_desktop_drop_logged_.exchange(
true, std::memory_order_relaxed)) {
LOG_WARN(
"Windows capturer dropping post-secure-desktop frame from "
"unknown display: display='{}', mapped='{}', size={}x{}, "
"bytes={}",
raw_display_name, mapped_name, w, h, size);
}
return;
}
}
if (post_secure_desktop_waiting_for_frame_.exchange(
false, std::memory_order_relaxed)) {
const ULONGLONG start_tick =
post_secure_desktop_started_tick_.exchange(
0, std::memory_order_relaxed);
const ULONGLONG elapsed_ms =
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
LOG_INFO(
"Windows capturer first normal frame after secure desktop: "
"display='{}', mapped='{}', size={}x{}, bytes={}, elapsed_ms={}",
raw_display_name, mapped_name, w, h, size, elapsed_ms);
}
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
};
@@ -524,6 +551,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
running_.store(true, std::memory_order_relaxed);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); });
@@ -534,6 +565,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
int ScreenCapturerWin::Stop() {
running_.store(false, std::memory_order_relaxed);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
int ret = 0;
if (impl_) {
ret = impl_->Stop();
@@ -626,6 +661,93 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
}
}
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
if (!impl_ || !running_.load(std::memory_order_relaxed)) {
return false;
}
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
auto restore_monitor = [&]() {
RebuildAliasesFromImpl();
if (current_monitor > 0 && impl_->SwitchTo(current_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
};
auto try_started_backend = [&](std::unique_ptr<ScreenCapturer> cand,
const char* name,
bool is_wgc_plugin) -> bool {
if (!cand) {
return false;
}
const int init_ret = cand->Init(fps_, cb_);
if (init_ret != 0) {
LOG_WARN("Windows capturer: {} init after secure desktop failed (ret={})",
name, init_ret);
return false;
}
const int start_ret = cand->Start(show_cursor);
if (start_ret != 0) {
LOG_WARN(
"Windows capturer: {} start after secure desktop failed (ret={})",
name, start_ret);
cand->Destroy();
return false;
}
if (impl_) {
impl_->Destroy();
}
impl_ = std::move(cand);
impl_is_wgc_plugin_ = is_wgc_plugin;
restore_monitor();
LOG_INFO("Windows capturer: restarted {} after secure desktop", name);
return true;
};
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
impl_->Stop();
int ret = impl_->Start(show_cursor);
if (ret == 0) {
restore_monitor();
return true;
}
LOG_WARN(
"Windows capturer: capture backend restart after secure desktop failed "
"(ret={}), rebuilding backend",
ret);
impl_->Destroy();
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
ret = impl_->Start(show_cursor);
}
if (ret == 0) {
restore_monitor();
return true;
}
if (impl_is_wgc_plugin_ &&
try_started_backend(WgcPluginCapturer::Create(), "WGC plugin", true)) {
return true;
}
if (try_started_backend(std::make_unique<ScreenCapturerDxgi>(), "DXGI",
false)) {
return true;
}
if (try_started_backend(std::make_unique<ScreenCapturerGdi>(), "GDI",
false)) {
return true;
}
if (impl_) {
LOG_WARN(
"Windows capturer: all backend restart attempts after secure desktop "
"failed (last_ret={})",
ret);
}
return false;
}
bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
int* height,
std::string* display_name) {
@@ -900,6 +1022,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::string last_stage;
std::string last_service_error;
ULONGLONG capture_stage_started_tick = 0;
bool post_secure_restart_pending = false;
ULONGLONG post_secure_restart_deadline_tick = 0;
ULONGLONG last_post_secure_restart_tick = 0;
SecureDesktopServiceStatus status;
std::vector<uint8_t> secure_frame;
@@ -951,6 +1076,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::memory_order_relaxed);
if (status.capture_active != last_capture_active ||
status.interactive_stage != last_stage) {
const bool secure_capture_started =
!last_capture_active && status.capture_active;
const bool secure_capture_ended =
last_capture_active && !status.capture_active;
capture_stage_started_tick = now;
LOG_INFO(
"Windows capturer secure desktop state: active={}, stage='{}', "
@@ -959,12 +1088,46 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
status.active_session_id);
last_capture_active = status.capture_active;
last_stage = status.interactive_stage;
if (secure_capture_started) {
post_secure_restart_pending = false;
post_secure_desktop_waiting_for_frame_.store(
false, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(
false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(
0, std::memory_order_relaxed);
} else if (secure_capture_ended) {
post_secure_restart_pending = true;
post_secure_restart_deadline_tick =
now + kPostSecureDesktopRestartTimeoutMs;
last_post_secure_restart_tick = 0;
post_secure_desktop_waiting_for_frame_.store(
true, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(
false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(
now, std::memory_order_relaxed);
}
}
last_status_tick = now;
}
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
if (post_secure_restart_pending) {
if (now >= post_secure_restart_deadline_tick) {
LOG_WARN(
"Windows capturer: capture backend restart after secure desktop "
"timed out");
post_secure_restart_pending = false;
} else if (last_post_secure_restart_tick == 0 ||
now - last_post_secure_restart_tick >=
kPostSecureDesktopRestartRetryMs) {
last_post_secure_restart_tick = now;
post_secure_restart_pending =
!RestartCaptureBackendAfterSecureDesktop();
}
}
std::this_thread::sleep_for(
std::chrono::milliseconds(status.service_available ? 50 : 200));
continue;
@@ -59,6 +59,9 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
std::atomic<bool> post_secure_desktop_drop_logged_{false};
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
std::thread secure_capture_thread_;
HANDLE secure_frame_mapping_ = nullptr;
HANDLE secure_frame_ready_event_ = nullptr;
@@ -77,6 +80,7 @@ class ScreenCapturerWin : public ScreenCapturer {
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
std::string* display_name);
+197 -150
View File
@@ -2,23 +2,10 @@
#include <Windows.Graphics.Capture.Interop.h>
#include <atomic>
#include <functional>
#include <memory>
#include <string>
#include "rd_log.h"
#define CHECK_INIT \
if (!is_initialized_) { \
LOG_ERROR("AE_NEED_INIT"); \
return 4; \
}
#define CHECK_CLOSED \
if (cleaned_.load() == true) { \
throw winrt::hresult_error(RO_E_CLOSED); \
}
namespace crossdesk {
extern "C" {
@@ -40,7 +27,7 @@ int WgcSessionImpl::Initialize(HWND hwnd) {
target_.hwnd = hwnd;
target_.is_window = true;
return Initialize();
return InitializeLocked();
}
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
@@ -48,7 +35,7 @@ int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
target_.hmonitor = hmonitor;
target_.is_window = false;
return Initialize();
return InitializeLocked();
}
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
@@ -59,68 +46,13 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
int WgcSessionImpl::Start(bool show_cursor) {
std::lock_guard locker(lock_);
if (is_running_) return 0;
int error = 1;
CHECK_INIT;
try {
last_show_cursor_ = show_cursor;
if (!capture_session_) {
auto current_size = capture_item_.Size();
capture_framepool_ =
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
CreateFreeThreaded(d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, current_size);
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
capture_frame_size_ = current_size;
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
capture_close_trigger_ = capture_item_.Closed(
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
}
if (!capture_framepool_) throw std::exception();
is_running_ = true;
// we do not need to crate a thread to enter a message loop coz we use
// CreateFreeThreaded instead of Create to create a capture frame pool,
// we need to test the performance later
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
error = 0;
} catch (winrt::hresult_error) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
return 86;
}
return error;
return StartLocked(show_cursor);
}
int WgcSessionImpl::Stop() {
std::lock_guard locker(lock_);
CHECK_INIT;
is_running_ = false;
// if (loop_.joinable()) loop_.join();
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
if (capture_session_) {
capture_session_.Close();
capture_session_ = nullptr;
}
CleanUpLocked();
return 0;
}
@@ -129,7 +61,10 @@ int WgcSessionImpl::Pause() {
is_paused_ = true;
CHECK_INIT;
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0;
}
@@ -138,7 +73,10 @@ int WgcSessionImpl::Resume() {
is_paused_ = false;
CHECK_INIT;
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0;
}
@@ -175,10 +113,10 @@ auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
interop_factory->CreateForWindow(
winrt::check_hresult(interop_factory->CreateForWindow(
hwnd,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item)));
reinterpret_cast<void**>(winrt::put_abi(item))));
return item;
}
@@ -187,10 +125,10 @@ auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
interop_factory->CreateForMonitor(
winrt::check_hresult(interop_factory->CreateForMonitor(
hmonitor,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item)));
reinterpret_cast<void**>(winrt::put_abi(item))));
return item;
}
@@ -218,6 +156,104 @@ HRESULT WgcSessionImpl::CreateMappedTexture(
d3d11_texture_mapped_.put());
}
int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
if (!capture_item_) {
LOG_ERROR("WGC: capture item is null");
return 4;
}
try {
if (!capture_session_) {
auto current_size = capture_item_.Size();
capture_framepool_ =
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
CreateFreeThreaded(d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, current_size);
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
capture_frame_size_ = current_size;
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
capture_close_trigger_ = capture_item_.Closed(
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
}
if (!capture_framepool_ || !capture_session_) {
throw std::exception();
}
capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
is_running_ = true;
return 0;
} catch (const winrt::hresult_error&) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
}
}
int WgcSessionImpl::StartLocked(bool show_cursor) {
if (is_running_) return 0;
last_show_cursor_ = show_cursor;
if (!is_initialized_) {
const int init_ret = InitializeLocked();
if (init_ret != 0) {
return init_ret;
}
}
int ret = StartCaptureLocked(show_cursor);
if (ret == 0) {
return 0;
}
LOG_WARN("WGC: start capture failed, rebuilding capture item");
CleanUpLocked();
ret = InitializeLocked();
if (ret != 0) {
return ret;
}
return StartCaptureLocked(show_cursor);
}
void WgcSessionImpl::StopLocked() {
is_running_ = false;
// if (loop_.joinable()) loop_.join();
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
if (capture_close_trigger_) capture_close_trigger_.revoke();
if (capture_session_) {
try {
capture_session_.Close();
} catch (...) {
LOG_WARN("WGC: capture session close failed");
}
capture_session_ = nullptr;
}
if (capture_framepool_) {
try {
capture_framepool_.Close();
} catch (...) {
LOG_WARN("WGC: frame pool close failed");
}
capture_framepool_ = nullptr;
}
d3d11_texture_mapped_ = nullptr;
}
void WgcSessionImpl::OnFrame(
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
@@ -225,7 +261,7 @@ void WgcSessionImpl::OnFrame(
auto is_new_size = false;
{
try {
auto frame = sender.TryGetNextFrame();
auto frame_size = frame.ContentSize();
@@ -239,60 +275,63 @@ void WgcSessionImpl::OnFrame(
}
// copy to mapped texture
{
if (is_paused_) {
return;
}
auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size) {
HRESULT tex_hr = CreateMappedTexture(frame_captured);
if (FAILED(tex_hr)) {
OutputDebugStringW(
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
.c_str());
return;
}
}
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
frame_captured.get());
D3D11_MAPPED_SUBRESOURCE map_result;
HRESULT hr = d3d11_device_context_->Map(
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
0 /*coz we use CreateFreeThreaded, so we cant use flags
D3D11_MAP_FLAG_DO_NOT_WAIT*/
,
&map_result);
if (FAILED(hr)) {
OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
}
// copy data from map_result.pData
if (map_result.pData && observer_) {
observer_->OnFrame(
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
static_cast<unsigned int>(frame_size.Height),
map_result.RowPitch,
const_cast<const unsigned char*>(
(unsigned char*)map_result.pData)},
id_);
}
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
if (is_paused_) {
return;
}
}
if (is_new_size) {
capture_framepool_.Recreate(d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, capture_frame_size_);
auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size) {
HRESULT tex_hr = CreateMappedTexture(frame_captured);
if (FAILED(tex_hr)) {
OutputDebugStringW(
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
.c_str());
return;
}
}
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
frame_captured.get());
D3D11_MAPPED_SUBRESOURCE map_result;
HRESULT hr = d3d11_device_context_->Map(
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
0 /*coz we use CreateFreeThreaded, so we cant use flags
D3D11_MAP_FLAG_DO_NOT_WAIT*/
,
&map_result);
if (FAILED(hr)) {
OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
}
// copy data from map_result.pData
if (map_result.pData && observer_) {
observer_->OnFrame(
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
static_cast<unsigned int>(frame_size.Height),
map_result.RowPitch,
const_cast<const unsigned char*>(
(unsigned char*)map_result.pData)},
id_);
}
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
if (is_new_size) {
capture_framepool_.Recreate(
d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, capture_frame_size_);
}
} catch (const winrt::hresult_error&) {
LOG_WARN("WGC: frame processing failed");
} catch (...) {
LOG_WARN("WGC: frame processing failed");
}
}
@@ -300,11 +339,13 @@ void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) {
std::lock_guard locker(lock_);
const bool was_running = is_running_;
const bool was_paused = is_paused_;
try {
CleanUp();
is_initialized_ = false;
if (Initialize() == 0) {
int ret = Start(last_show_cursor_);
CleanUpLocked();
is_paused_ = was_paused;
if (InitializeLocked() == 0) {
int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
if (ret == 0) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
} else {
@@ -319,9 +360,14 @@ void WgcSessionImpl::OnClosed(
}
}
int WgcSessionImpl::Initialize() {
int WgcSessionImpl::InitializeLocked() {
if (is_initialized_) return 0;
d3d11_texture_mapped_ = nullptr;
d3d11_device_context_ = nullptr;
d3d11_direct_device_ = nullptr;
capture_frame_size_ = {};
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
return 1;
@@ -332,6 +378,10 @@ int WgcSessionImpl::Initialize() {
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
else
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
if (!capture_item_) {
LOG_ERROR("WGC: create capture item returned null");
return 86;
}
// Set up
auto d3d11_device =
@@ -353,21 +403,18 @@ int WgcSessionImpl::Initialize() {
void WgcSessionImpl::CleanUp() {
std::lock_guard locker(lock_);
auto expected = false;
if (cleaned_.compare_exchange_strong(expected, true)) {
capture_close_trigger_.revoke();
capture_framepool_trigger_.revoke();
CleanUpLocked();
}
if (capture_framepool_) capture_framepool_.Close();
void WgcSessionImpl::CleanUpLocked() {
StopLocked();
if (capture_session_) capture_session_.Close();
capture_framepool_ = nullptr;
capture_session_ = nullptr;
capture_item_ = nullptr;
is_initialized_ = false;
}
capture_item_ = nullptr;
d3d11_device_context_ = nullptr;
d3d11_direct_device_ = nullptr;
capture_frame_size_ = {};
is_initialized_ = false;
is_paused_ = false;
}
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
@@ -68,8 +68,12 @@ class WgcSessionImpl : public WgcSession {
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&);
int Initialize();
int InitializeLocked();
int StartLocked(bool show_cursor);
int StartCaptureLocked(bool show_cursor);
void StopLocked();
void CleanUp();
void CleanUpLocked();
// void message_func();
@@ -94,7 +98,6 @@ class WgcSessionImpl : public WgcSession {
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
std::atomic<bool> cleaned_ = false;
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
capture_framepool_{nullptr};
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
+28 -6
View File
@@ -591,6 +591,14 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
return "user-desktop";
}
bool IsCredentialUiVisible(bool prelogin, bool session_locked,
bool logon_ui_running,
bool input_desktop_available,
bool secure_desktop_active) {
return (prelogin || session_locked || secure_desktop_active) &&
(logon_ui_running || !input_desktop_available);
}
std::wstring SecureInputHelperDesktopForStage(
const std::string& interactive_stage) {
if (interactive_stage == "credential-ui" ||
@@ -1323,8 +1331,13 @@ bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
}
bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
const bool service_host_credential_ui_visible =
!session_helper_status_ok_ &&
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
input_desktop_available_,
secure_desktop_active_);
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
secure_desktop_active_ || logon_ui_visible_ ||
secure_desktop_active_ || service_host_credential_ui_visible ||
session_helper_report_credential_ui_visible_ ||
session_helper_report_secure_desktop_active_ ||
session_helper_report_unlock_ui_visible_ ||
@@ -1377,9 +1390,14 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
return session_helper_report_interactive_stage_;
}
const bool service_host_credential_ui_visible =
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
input_desktop_available_,
secure_desktop_active_);
return DetermineInteractiveStage(
IsHelperReportingLockScreenLocked(),
session_helper_report_credential_ui_visible_ || logon_ui_visible_,
session_helper_report_credential_ui_visible_ ||
service_host_credential_ui_visible,
session_helper_report_secure_desktop_active_ || secure_desktop_active_);
}
@@ -1991,12 +2009,17 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
interactive_state_ready
? (effective_session_locked && IsHelperReportingLockScreenLocked())
: false;
const bool service_host_credential_ui_visible =
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
input_desktop_available_,
secure_desktop_active_);
bool credential_ui_visible =
interactive_state_ready ? session_helper_report_credential_ui_visible_
: logon_ui_visible_;
: service_host_credential_ui_visible;
bool unlock_ui_visible = interactive_state_ready
? session_helper_report_unlock_ui_visible_
: (logon_ui_visible_ || secure_desktop_active_);
: (credential_ui_visible ||
secure_desktop_active_);
unlock_ui_visible = unlock_ui_visible || sas_secure_desktop_grace_active;
bool interactive_secure_desktop_active =
interactive_state_ready ? session_helper_report_secure_desktop_active_
@@ -2004,8 +2027,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
interactive_secure_desktop_active =
interactive_secure_desktop_active || sas_secure_desktop_grace_active;
bool interactive_logon_ui_visible =
interactive_state_ready ? session_helper_report_logon_ui_visible_
: logon_ui_visible_;
credential_ui_visible;
bool interactive_session_locked = effective_session_locked ||
interactive_lock_screen_visible ||
unlock_ui_visible ||
+14 -4
View File
@@ -340,6 +340,13 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
return "user-desktop";
}
bool IsCredentialUiVisible(bool session_locked, bool logon_ui_running,
bool input_desktop_available,
bool secure_desktop_active) {
return (session_locked || secure_desktop_active) &&
(logon_ui_running || !input_desktop_available);
}
std::string BuildErrorJson(const char* error, DWORD error_code = 0) {
Json json;
json["ok"] = false;
@@ -392,9 +399,10 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
Json json;
std::lock_guard<std::mutex> lock(helper_state->mutex);
const bool credential_ui_visible =
helper_state->logon_ui_visible ||
(helper_state->session_locked && !helper_state->input_desktop_available);
const bool credential_ui_visible = IsCredentialUiVisible(
helper_state->session_locked, helper_state->logon_ui_visible,
helper_state->input_desktop_available,
helper_state->secure_desktop_active);
const bool unlock_ui_visible =
credential_ui_visible || helper_state->secure_desktop_active;
json["ok"] = true;
@@ -1901,7 +1909,9 @@ int main(int argc, char* argv[]) {
secure_desktop_active = helper_state.secure_desktop_active;
}
const bool credential_ui_visible =
logon_ui_running || (session_locked && !input_desktop_available);
IsCredentialUiVisible(session_locked, logon_ui_running,
input_desktop_available,
secure_desktop_active);
std::string stage = DetermineInteractiveStage(
lock_app_visible, credential_ui_visible, secure_desktop_active);
+86
View File
@@ -39,6 +39,35 @@ bool ExpectContains(const char* name, const std::string& value,
return false;
}
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
return false;
}
bool ExpectContainsAtLeast(const char* name, const std::string& value,
const std::string& expected, size_t min_count) {
size_t count = 0;
size_t pos = 0;
while ((pos = value.find(expected, pos)) != std::string::npos) {
++count;
pos += expected.size();
}
if (count >= min_count) {
return true;
}
std::cerr << name << " expected at least " << min_count
<< " occurrences of: " << expected << ", found " << count
<< "\n";
return false;
}
bool ExpectResetBeforeDisplayPopup(const std::string& value) {
const std::string reset = "props->display_selectable_hovered_ = false;";
const std::string popup = "ImGui::BeginPopup(\"display\")";
@@ -93,5 +122,62 @@ int main() {
ok &= ExpectContains("control_bar.cpp", control_bar,
"props->shortcut_selectable_hovered_ =");
ok &= ExpectResetBeforeShortcutPopup(control_bar);
ok &= ExpectContains("control_bar.cpp", control_bar,
"void ShowControlBarTooltip(const std::string& text)");
ok &= ExpectContainsAtLeast("control_bar.cpp", control_bar,
"ShowControlBarTooltip(", 10);
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::select_display"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::send_shortcut"
"[localization_language_index_]");
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"ShowControlBarTooltip("
"props->mouse_control_button_label_)");
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"ShowControlBarTooltip("
"props->audio_capture_button_label_)");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::select_file"
"[localization_language_index_]");
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"ShowControlBarTooltip("
"props->net_traffic_stats_button_label_)");
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"ShowControlBarTooltip("
"props->fullscreen_button_label_)");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::release_mouse"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::control_mouse"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::audio_capture"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::mute[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::hide_net_traffic_stats"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::show_net_traffic_stats"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::exit_fullscreen"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::fullscreen"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::disconnect"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::expand_control_bar"
"[localization_language_index_]");
ok &= ExpectContains("control_bar.cpp", control_bar,
"localization::collapse_control_bar"
"[localization_language_index_]");
return ok ? 0 : 1;
}