mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-24 12:11:27 +08:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df72204619 | ||
|
|
3678271577 | ||
|
|
6b5535ab3d | ||
|
|
078bd1e520 | ||
|
|
718705c352 | ||
|
|
1ca5852280 | ||
|
|
1eda6cc183 | ||
|
|
8d37f8f5fb | ||
|
|
05b8aa629e | ||
|
|
2cbceba953 | ||
|
|
89d3c8b53f | ||
|
|
51a4190e97 | ||
|
|
3737cd0dd5 | ||
|
|
00957d6740 | ||
|
|
6ec86322b2 | ||
|
|
ecc131b5ee | ||
|
|
1aa50e9ffa | ||
|
|
d49f5bc520 | ||
|
|
8818b9b8be | ||
|
|
3be283238c | ||
|
|
5d0453fff3 | ||
|
|
8a9775a571 | ||
|
|
7f4f1882ae | ||
|
|
23e24e3d47 | ||
|
|
7e1b9cb00e | ||
|
|
0c70acbdbf | ||
|
|
fcbbb0f51c | ||
|
|
f4be0700bf | ||
|
|
a45670b04d | ||
|
|
ff329cae81 | ||
|
|
092664784e | ||
|
|
1709eb0efb | ||
|
|
d27bb5d7a4 |
@@ -260,7 +260,7 @@ jobs:
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
|
||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package CrossDesk app
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# CrossDesk Privacy Policy
|
||||
|
||||
Last updated: August 8, 2026
|
||||
|
||||
[中文](#中文) | [English](#english)
|
||||
|
||||
## 中文
|
||||
|
||||
### 适用范围
|
||||
|
||||
本隐私政策适用于 CrossDesk 桌面客户端及项目提供的默认网络服务。用户自行部署或指定的服务器由相应服务器运营者负责,其数据处理方式不在本政策控制范围内。
|
||||
|
||||
### 网络通信
|
||||
|
||||
CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接协商及远程会话。客户端可能进行以下网络通信:
|
||||
|
||||
- **默认服务:** 未启用自托管时,客户端会连接预配置的 CrossDesk 服务(`api.crossdesk.cn`),用于设备注册、信令、网络地址转换(NAT)穿透,并在无法建立直接连接时提供中继。
|
||||
- **版本检查:** 客户端启动时会从 CrossDesk 版本服务(`version.crossdesk.cn`)获取最新版本信息。当前即使启用自托管,版本检查仍会访问 CrossDesk 版本服务。
|
||||
- **远程会话:** 用户主动发起或接受远程连接后,CrossDesk 会根据使用的功能在设备之间传输屏幕画面、音频、键盘和鼠标输入、剪贴板文本以及用户选择传输的文件。剪贴板文本可能在远程会话期间自动同步。这些数据可能通过直接连接传输,也可能在必要时通过配置的中继服务器转发。
|
||||
- **自托管:** 用户可以在设置中指定自己的信令及中继服务器。自托管服务器的日志、数据保存和访问规则由其运营者决定。
|
||||
|
||||
### 处理的信息
|
||||
|
||||
为提供上述功能,CrossDesk 可能处理:
|
||||
|
||||
- 连接所需的设备标识及认证信息;
|
||||
- IP 地址、端口、连接时间、连接状态等必要的网络元数据;
|
||||
- 版本检查请求产生的标准网络请求信息;
|
||||
- 用户授权的远程会话中处理的屏幕、音频、控制指令、剪贴板及文件内容。
|
||||
|
||||
### 信息的使用与保存
|
||||
|
||||
- 设备标识、连接设置及用户偏好会保存在本地设备上,以便后续使用。
|
||||
- 通过官方信令和中继服务处理的信息仅用于建立、维持和转发 CrossDesk 连接,以及服务运行、安全防护和故障排查。
|
||||
- 中继传输的远程会话内容仅用于实时转发,不用于广告或用户画像。
|
||||
- CrossDesk 桌面客户端不包含广告功能,也不会主动上传诊断日志、崩溃报告或用户行为分析数据。
|
||||
- 自托管服务器可能保存的日志和数据由其运营者控制,请向相应运营者了解其隐私与保存政策。
|
||||
|
||||
### 用户选择
|
||||
|
||||
用户可以:
|
||||
|
||||
- 配置自托管服务器,以替代默认信令和中继服务;
|
||||
- 决定是否发起或接受远程会话;
|
||||
- 决定是否选择并传输文件;
|
||||
- 结束远程会话,以停止会话数据传输;
|
||||
- 删除 CrossDesk 的本地配置、缓存及日志文件。
|
||||
|
||||
### 政策更新与联系方式
|
||||
|
||||
本政策可能随着 CrossDesk 功能或网络服务的变化而更新。更新内容将在本文件中发布。如有隐私相关问题,请联系 [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com)。
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
### Scope
|
||||
|
||||
This Privacy Policy applies to the CrossDesk desktop client and the default network services provided by the project. Servers deployed or selected by users are operated by their respective administrators, and their data-processing practices are outside the control of this policy.
|
||||
|
||||
### Network communications
|
||||
|
||||
CrossDesk is remote desktop software and requires network communications for device discovery, connection negotiation, and remote sessions. The client may perform the following communications:
|
||||
|
||||
- **Default services:** Unless self-hosting is enabled, the client connects to preconfigured CrossDesk services (`api.crossdesk.cn`) for device registration, signaling, network address translation (NAT) traversal, and relay when a direct connection cannot be established.
|
||||
- **Update checks:** On startup, the client retrieves current release information from the CrossDesk version service (`version.crossdesk.cn`). Update checks currently continue to use the CrossDesk version service even when self-hosting is enabled.
|
||||
- **Remote sessions:** After a user initiates or accepts a remote connection, CrossDesk transmits the data required by enabled features between devices. This may include screen content, audio, keyboard and mouse input, clipboard text, and files selected by the user. Clipboard text may be synchronized automatically during a remote session. Data may travel over a direct connection or through the configured relay server when necessary.
|
||||
- **Self-hosting:** Users may configure their own signaling and relay servers. Logging, retention, and access policies for a self-hosted server are determined by its operator.
|
||||
|
||||
### Information processed
|
||||
|
||||
To provide these functions, CrossDesk may process:
|
||||
|
||||
- device identifiers and authentication information required for connections;
|
||||
- necessary network metadata, such as IP addresses, ports, connection times, and connection status;
|
||||
- standard network request information generated by update checks; and
|
||||
- screen, audio, control, clipboard, and file content processed during a remote session authorized by the user.
|
||||
|
||||
### Use and storage of information
|
||||
|
||||
- Device identifiers, connection settings, and user preferences are stored locally for subsequent use.
|
||||
- Information processed by the official signaling and relay services is used only to establish, maintain, and relay CrossDesk connections and for service operation, security, and troubleshooting.
|
||||
- Remote-session content passing through a relay is used only for real-time forwarding and is not used for advertising or user profiling.
|
||||
- The CrossDesk desktop client does not contain advertising functionality and does not intentionally upload diagnostic logs, crash reports, or behavioral analytics.
|
||||
- Logs and data that may be retained by self-hosted servers are controlled by their operators. Contact the relevant operator for its privacy and retention practices.
|
||||
|
||||
### User choices
|
||||
|
||||
Users may:
|
||||
|
||||
- configure a self-hosted server instead of the default signaling and relay services;
|
||||
- choose whether to initiate or accept a remote session;
|
||||
- choose whether to select and transfer files;
|
||||
- end a remote session to stop session-data transmission; and
|
||||
- remove local CrossDesk configuration, cache, and log files.
|
||||
|
||||
### Changes and contact
|
||||
|
||||
This policy may be updated as CrossDesk features or network services change. Updates will be published in this file. For privacy-related questions, contact [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com).
|
||||
@@ -430,4 +430,4 @@ CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供
|
||||
|
||||
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
|
||||
- **Privacy policy(隐私政策):** 除非用户或安装、运行 CrossDesk 的人员明确要求,否则 CrossDesk 不会向其他联网系统传输任何信息。
|
||||
- **隐私政策:** [查看 CrossDesk 隐私政策](PRIVACY.md#中文)
|
||||
|
||||
+1
-1
@@ -447,4 +447,4 @@ CrossDesk uses SignPath.io to code-sign official Windows releases built from thi
|
||||
|
||||
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers:** [kunkundi](https://github.com/kunkundi)
|
||||
- **Privacy policy:** CrossDesk will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
|
||||
- **Privacy policy:** [Read the CrossDesk Privacy Policy](PRIVACY.md#english)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||
#define CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// MiniRTC stream identifiers are protocol-facing logical identifiers. Keep
|
||||
// them independent from platform display handles and user-visible names, both
|
||||
// of which may change or contain duplicate/Unicode text.
|
||||
inline std::string MakeDisplayStreamId(size_t display_index) {
|
||||
return "Display" + std::to_string(display_index + 1);
|
||||
}
|
||||
|
||||
inline bool IsRegisteredDisplayStreamId(const std::string& stream_id,
|
||||
size_t display_count) {
|
||||
if (stream_id.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < display_count; ++index) {
|
||||
if (stream_id == MakeDisplayStreamId(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolves a backend-reported identifier to a stream registered with MiniRTC.
|
||||
// Backends should report MakeDisplayStreamId(index); the fallbacks keep older
|
||||
// capture plugins and hotplug transitions safe.
|
||||
inline std::string ResolveDisplayStreamId(
|
||||
const char* reported_id, size_t display_count, int preferred_index = -1,
|
||||
const std::string& previous_id = {}) {
|
||||
const std::string candidate = reported_id ? reported_id : "";
|
||||
if (IsRegisteredDisplayStreamId(candidate, display_count)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (preferred_index >= 0 &&
|
||||
preferred_index < static_cast<int>(display_count)) {
|
||||
return MakeDisplayStreamId(static_cast<size_t>(preferred_index));
|
||||
}
|
||||
|
||||
if (IsRegisteredDisplayStreamId(previous_id, display_count)) {
|
||||
return previous_id;
|
||||
}
|
||||
|
||||
if (display_count == 1) {
|
||||
return MakeDisplayStreamId(0);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||
@@ -29,7 +29,11 @@ int ConfigCenter::Load() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool persist_turn_mode_migration = false;
|
||||
bool persist_config_migration = false;
|
||||
persist_config_migration |= ini_.Delete(section_, "video_content_type");
|
||||
persist_config_migration |= ini_.Delete(section_, "screen_content");
|
||||
persist_config_migration |=
|
||||
ini_.Delete(section_, "enable_desktop_quality_optimization");
|
||||
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
@@ -52,7 +56,6 @@ int ConfigCenter::Load() {
|
||||
|
||||
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
||||
hardware_video_codec_);
|
||||
|
||||
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
|
||||
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
||||
const long parsed_turn_mode = ini_.GetLongValue(
|
||||
@@ -70,7 +73,7 @@ int ConfigCenter::Load() {
|
||||
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
|
||||
: TURN_MODE::DISABLED;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
persist_turn_mode_migration = true;
|
||||
persist_config_migration = true;
|
||||
}
|
||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
enable_self_hosted_ =
|
||||
@@ -121,8 +124,7 @@ int ConfigCenter::Load() {
|
||||
file_transfer_save_path_ = "";
|
||||
}
|
||||
|
||||
if (persist_turn_mode_migration &&
|
||||
ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <shared_mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -51,6 +52,15 @@ namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
|
||||
const auto timestamp = std::chrono::system_clock::now()
|
||||
.time_since_epoch()
|
||||
.count();
|
||||
std::random_device random;
|
||||
return std::to_string(timestamp) + "-" + std::to_string(random()) + "-" +
|
||||
std::to_string(sequence);
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool HasNonEmptyEnvironmentVariable(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
@@ -1342,33 +1352,72 @@ void GuiApplication::BindMainCallbacks() {
|
||||
if (password.size() != 6) {
|
||||
return false;
|
||||
}
|
||||
std::memset(password_saved_, 0, sizeof(password_saved_));
|
||||
std::strncpy(password_saved_, password.c_str(),
|
||||
sizeof(password_saved_) - 1);
|
||||
|
||||
if (config_center_->IsSelfHosted()) {
|
||||
std::string identity = self_hosted_id_;
|
||||
if (const auto at = identity.find('@'); at != std::string::npos) {
|
||||
identity.resize(at);
|
||||
}
|
||||
if (identity.empty()) {
|
||||
identity = client_id_;
|
||||
}
|
||||
identity += "@" + password;
|
||||
std::memset(self_hosted_id_, 0, sizeof(self_hosted_id_));
|
||||
std::strncpy(self_hosted_id_, identity.c_str(),
|
||||
sizeof(self_hosted_id_) - 1);
|
||||
} else {
|
||||
const std::string identity = std::string(client_id_) + "@" + password;
|
||||
std::memset(client_id_with_password_, 0,
|
||||
sizeof(client_id_with_password_));
|
||||
std::strncpy(client_id_with_password_, identity.c_str(),
|
||||
sizeof(client_id_with_password_) - 1);
|
||||
if (!peer_ || !signal_connected_) {
|
||||
offline_warning_text_ =
|
||||
localization::signal_disconnected[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
return true;
|
||||
}
|
||||
settings_.Save();
|
||||
if (peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
|
||||
std::string request_id;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
if (password_change_pending_) {
|
||||
return true;
|
||||
}
|
||||
request_id =
|
||||
CreatePasswordChangeRequestId(++next_password_change_request_id_);
|
||||
password_change_pending_ = true;
|
||||
password_change_result_ready_ = false;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = false;
|
||||
password_change_requested_at_ = std::chrono::steady_clock::now();
|
||||
pending_password_change_request_id_ = request_id;
|
||||
pending_local_password_ = password;
|
||||
password_change_error_.clear();
|
||||
}
|
||||
|
||||
const bool self_hosted = config_center_->IsSelfHosted();
|
||||
const std::string server_host =
|
||||
self_hosted ? config_center_->GetSignalServerHost()
|
||||
: config_center_->GetDefaultServerHost();
|
||||
const int server_port =
|
||||
self_hosted ? config_center_->GetSignalServerPort()
|
||||
: config_center_->GetDefaultSignalServerPort();
|
||||
const std::string pending_identity =
|
||||
std::string(client_id_) + "@" + password;
|
||||
if (!settings_.StagePendingPasswordChange(
|
||||
pending_identity, self_hosted, server_host, server_port,
|
||||
request_id)) {
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
password_change_pending_ = false;
|
||||
pending_password_change_request_id_.clear();
|
||||
pending_local_password_.clear();
|
||||
password_change_error_.clear();
|
||||
offline_warning_text_ = localization::failed[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_ERROR("Could not durably stage password change");
|
||||
return true;
|
||||
}
|
||||
|
||||
const nlohmann::json request = {{"type", "change_password"},
|
||||
{"request_id", request_id},
|
||||
{"new_password", password}};
|
||||
const std::string message = request.dump();
|
||||
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
password_change_pending_ = false;
|
||||
pending_password_change_request_id_.clear();
|
||||
pending_local_password_.clear();
|
||||
offline_warning_text_ =
|
||||
localization::signal_disconnected[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
}
|
||||
if (!settings_.ClearPendingPasswordChange()) {
|
||||
LOG_WARN("Could not clear unsent pending password change");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -1778,6 +1827,8 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
if (!peer_) {
|
||||
CreateConnectionPeer();
|
||||
}
|
||||
@@ -1852,6 +1903,120 @@ void GuiApplication::Tick() {
|
||||
SyncServerWindow();
|
||||
}
|
||||
|
||||
void GuiApplication::HandlePasswordChangeResult() {
|
||||
bool succeeded = false;
|
||||
bool uncertain = false;
|
||||
std::string error;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
if (password_change_pending_ && !password_change_result_ready_ &&
|
||||
std::chrono::steady_clock::now() - password_change_requested_at_ >=
|
||||
std::chrono::seconds(10)) {
|
||||
password_change_result_ready_ = true;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = true;
|
||||
password_change_error_ = "Server did not respond";
|
||||
}
|
||||
|
||||
if (!password_change_result_ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
succeeded = password_change_succeeded_;
|
||||
uncertain = password_change_result_uncertain_;
|
||||
error = password_change_error_;
|
||||
password_change_pending_ = false;
|
||||
password_change_result_ready_ = false;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = false;
|
||||
pending_password_change_request_id_.clear();
|
||||
pending_local_password_.clear();
|
||||
password_change_error_.clear();
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
if (uncertain) {
|
||||
LOG_WARN("Password change outcome is unknown: {}; re-authenticating",
|
||||
error);
|
||||
} else {
|
||||
LOG_WARN("Password change failed: {}", error);
|
||||
if (!settings_.ClearPendingPasswordChange()) {
|
||||
LOG_WARN("Could not clear rejected pending password change");
|
||||
}
|
||||
}
|
||||
offline_warning_text_ = localization::failed[localization_language_index_];
|
||||
if (!error.empty()) {
|
||||
offline_warning_text_ += ": " + error;
|
||||
}
|
||||
show_offline_warning_window_ = true;
|
||||
if (uncertain && peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings_.PromotePendingPasswordChange()) {
|
||||
// The pending credential was persisted before the request was sent, so it
|
||||
// remains sufficient for recovery even when promotion cannot be written.
|
||||
LOG_WARN("Password changed on server; pending recovery record retained");
|
||||
}
|
||||
|
||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
||||
if (peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::HandleCredentialRecovery() {
|
||||
bool retry_active = false;
|
||||
bool promote_pending = false;
|
||||
bool clear_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
retry_active = credential_recovery_retry_active_;
|
||||
promote_pending = credential_recovery_promote_pending_;
|
||||
clear_pending = credential_recovery_clear_pending_;
|
||||
credential_recovery_retry_active_ = false;
|
||||
credential_recovery_promote_pending_ = false;
|
||||
credential_recovery_clear_pending_ = false;
|
||||
if (retry_active) {
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (retry_active) {
|
||||
LOG_INFO("Pending credential was not accepted; retrying active credential");
|
||||
if (peer_) {
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (promote_pending) {
|
||||
if (!settings_.PromotePendingPasswordChange()) {
|
||||
LOG_WARN("Recovered credential is active but promotion remains pending");
|
||||
} else {
|
||||
LOG_INFO("Recovered password change with pending credential");
|
||||
}
|
||||
} else if (clear_pending) {
|
||||
if (!settings_.ClearPendingPasswordChange()) {
|
||||
LOG_WARN("Active credential recovered but pending record could not be "
|
||||
"cleared");
|
||||
} else {
|
||||
LOG_INFO("Password change was not committed; retained active credential");
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
credential_recovery_in_progress_ = false;
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
}
|
||||
|
||||
void GuiApplication::UpdateLocalization() {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(localization_language_index_);
|
||||
@@ -2449,8 +2614,10 @@ void GuiApplication::SyncStreamWindow() {
|
||||
|
||||
std::vector<slint::SharedString> displays;
|
||||
displays.reserve(props->display_info_list_.size());
|
||||
for (const auto& display : props->display_info_list_) {
|
||||
displays.emplace_back(UiText(display.name));
|
||||
for (size_t index = 0; index < props->display_info_list_.size(); ++index) {
|
||||
displays.emplace_back(UiText(localization::FormatDisplayLabel(
|
||||
index, props->display_info_list_[index].name,
|
||||
localization_language_index_)));
|
||||
}
|
||||
ui_->display_model->set_vector(std::move(displays));
|
||||
(*ui_->stream)->set_selected_display(props->selected_display_);
|
||||
|
||||
@@ -31,6 +31,8 @@ private:
|
||||
void BindStreamCallbacks();
|
||||
void BindServerCallbacks();
|
||||
void Tick();
|
||||
void HandlePasswordChangeResult();
|
||||
void HandleCredentialRecovery();
|
||||
void SyncMainWindow();
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -110,12 +111,26 @@ inline const std::string& GetTranslatedText(const std::string& key,
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
}
|
||||
|
||||
inline std::string FormatDisplayLabel(size_t display_index,
|
||||
const std::string& display_name,
|
||||
int language_index) {
|
||||
const std::string number = std::to_string(display_index + 1);
|
||||
std::string label =
|
||||
detail::GetTranslatedText("display_screen", language_index) + " " +
|
||||
number;
|
||||
const std::string fallback_name = "Display" + number;
|
||||
if (!display_name.empty() && display_name != fallback_name) {
|
||||
label += " (" + display_name + ")";
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
@@ -35,6 +35,7 @@ struct TranslationRow {
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(display_screen, u8"显示屏", "Display", u8"Экран") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -47,6 +48,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
? 30
|
||||
: 60;
|
||||
LOG_INFO("Init screen capturer with {} fps", fps);
|
||||
display_info_list_.clear();
|
||||
registered_display_stream_count_ = 0;
|
||||
last_video_frame_stream_id_.clear();
|
||||
invalid_video_stream_id_logged_ = false;
|
||||
|
||||
const int init_ret = screen_capturer_->Init(
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
@@ -60,7 +65,41 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string stream_id = display_name ? display_name : "";
|
||||
std::vector<std::string> connected_remote_ids;
|
||||
{
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
connected_remote_ids.reserve(owner_.connection_status_.size());
|
||||
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
connected_remote_ids.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture can still deliver frames while ICE is gathering or after
|
||||
// the final controller disconnects. Do not broadcast those frames to
|
||||
// MiniRTC: a broadcast also reaches newly joining peers whose ICE
|
||||
// transport is not ready yet.
|
||||
if (connected_remote_ids.empty()) {
|
||||
last_frame_time_ = now_time;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string stream_id = ResolveDisplayStreamId(
|
||||
display_name, registered_display_stream_count_, -1,
|
||||
last_video_frame_stream_id_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_video_stream_id_logged_) {
|
||||
LOG_ERROR(
|
||||
"Drop captured frames with an empty or unregistered video "
|
||||
"stream id, reported='{}', registered_streams={}",
|
||||
display_name ? display_name : "",
|
||||
registered_display_stream_count_);
|
||||
invalid_video_stream_id_logged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_video_stream_id_logged_ = false;
|
||||
const bool resumed_after_gap =
|
||||
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
|
||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||
@@ -79,7 +118,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
SendVideoFrame(owner_.peer_, &frame, stream_id.c_str());
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
}
|
||||
last_video_frame_stream_id_ = stream_id;
|
||||
last_frame_time_ = now_time;
|
||||
});
|
||||
@@ -90,6 +132,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
if (!latest_display_info.empty()) {
|
||||
display_info_list_ = latest_display_info;
|
||||
}
|
||||
registered_display_stream_count_ = display_info_list_.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,10 +77,12 @@ private:
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
size_t registered_display_stream_count_ = 0;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool invalid_video_stream_id_logged_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
#include "features/settings/settings_manager.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
@@ -14,6 +27,9 @@
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
|
||||
constexpr uint32_t kCacheV3Version = 3;
|
||||
|
||||
template <size_t Size>
|
||||
void CopyString(char (&destination)[Size], const char *source) {
|
||||
static_assert(Size > 0);
|
||||
@@ -23,6 +39,99 @@ void CopyString(char (&destination)[Size], const char *source) {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t CacheChecksum(const void *data, size_t size) {
|
||||
const auto *bytes = static_cast<const unsigned char *>(data);
|
||||
uint32_t hash = 2166136261u;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
hash ^= bytes[i];
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::filesystem::path TemporaryPathFor(
|
||||
const std::filesystem::path &target) {
|
||||
const auto timestamp = std::chrono::steady_clock::now()
|
||||
.time_since_epoch()
|
||||
.count();
|
||||
const auto thread_id =
|
||||
std::hash<std::thread::id>{}(std::this_thread::get_id());
|
||||
std::filesystem::path temporary = target;
|
||||
temporary += ".tmp-" + std::to_string(timestamp) + "-" +
|
||||
std::to_string(thread_id);
|
||||
return temporary;
|
||||
}
|
||||
|
||||
bool FlushFileToDisk(FILE *file) {
|
||||
if (!file || std::fflush(file) != 0) {
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return _commit(_fileno(file)) == 0;
|
||||
#else
|
||||
return fsync(fileno(file)) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ReplaceFileAtomically(const std::filesystem::path &temporary,
|
||||
const std::filesystem::path &target) {
|
||||
#if defined(_WIN32)
|
||||
return MoveFileExW(temporary.c_str(), target.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
if (::rename(temporary.c_str(), target.c_str()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path parent = target.parent_path().empty()
|
||||
? std::filesystem::path(".")
|
||||
: target.parent_path();
|
||||
int directory_flags = O_RDONLY;
|
||||
#if defined(O_DIRECTORY)
|
||||
directory_flags |= O_DIRECTORY;
|
||||
#endif
|
||||
const int directory = open(parent.c_str(), directory_flags);
|
||||
if (directory >= 0) {
|
||||
const bool synced = fsync(directory) == 0;
|
||||
close(directory);
|
||||
return synced;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WriteFileAtomically(const std::filesystem::path &target,
|
||||
const void *data, size_t size) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parent = target.parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const std::filesystem::path temporary = TemporaryPathFor(target);
|
||||
#if defined(_WIN32)
|
||||
FILE *file = _wfopen(temporary.c_str(), L"wb");
|
||||
#else
|
||||
FILE *file = std::fopen(temporary.c_str(), "wb");
|
||||
#endif
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = std::fwrite(data, 1, size, file) == size;
|
||||
const bool flushed = written && FlushFileToDisk(file);
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !flushed || !closed ||
|
||||
!ReplaceFileAtomically(temporary, target)) {
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
@@ -33,36 +142,74 @@ int SettingsManager::Save() {
|
||||
}
|
||||
|
||||
int SettingsManager::SaveLocked() {
|
||||
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_v2_file) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CopyString(cache_v2_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
|
||||
|
||||
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
|
||||
sizeof(cache_v2_));
|
||||
cache_v3_.magic = kCacheV3Magic;
|
||||
cache_v3_.version = kCacheV3Version;
|
||||
cache_v3_.base = cache_v2_;
|
||||
cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0';
|
||||
cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] =
|
||||
'\0';
|
||||
cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] =
|
||||
'\0';
|
||||
cache_v3_.checksum =
|
||||
CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum));
|
||||
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||
&cache_v3_, sizeof(cache_v3_))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
&cache_v2_, sizeof(cache_v2_))) {
|
||||
LOG_WARN("Failed to update legacy v2 credential cache");
|
||||
}
|
||||
|
||||
// Keep writing the legacy cache while older installations may still read it.
|
||||
std::ofstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||
std::ios::binary);
|
||||
if (cache_v1_file) {
|
||||
CopyString(cache_v1_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_),
|
||||
sizeof(cache_v1_));
|
||||
CopyString(cache_v1_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
|
||||
&cache_v1_, sizeof(cache_v1_))) {
|
||||
LOG_WARN("Failed to update legacy v1 credential cache");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SettingsManager::ReadV3Locked() {
|
||||
std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CacheV3 loaded{};
|
||||
cache_file.read(reinterpret_cast<char *>(&loaded), sizeof(loaded));
|
||||
if (cache_file.gcount() != static_cast<std::streamsize>(sizeof(loaded)) ||
|
||||
loaded.magic != kCacheV3Magic ||
|
||||
loaded.version != kCacheV3Version ||
|
||||
loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) {
|
||||
LOG_WARN("Ignore invalid v3 credential cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded.base.client_id_with_password
|
||||
[sizeof(loaded.base.client_id_with_password) - 1] = '\0';
|
||||
loaded.base.self_hosted_id[sizeof(loaded.base.self_hosted_id) - 1] = '\0';
|
||||
loaded.pending_identity[sizeof(loaded.pending_identity) - 1] = '\0';
|
||||
loaded.pending_server_host[sizeof(loaded.pending_server_host) - 1] = '\0';
|
||||
loaded.pending_request_id[sizeof(loaded.pending_request_id) - 1] = '\0';
|
||||
cache_v3_ = loaded;
|
||||
cache_v2_ = loaded.base;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsManager::ReadV2Locked() {
|
||||
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
@@ -84,13 +231,20 @@ bool SettingsManager::ReadV2Locked() {
|
||||
int SettingsManager::Load() {
|
||||
std::unique_lock<std::mutex> lock(cache_mutex_);
|
||||
|
||||
if (ReadV2Locked()) {
|
||||
const bool loaded_v3 = ReadV3Locked();
|
||||
if (loaded_v3 || ReadV2Locked()) {
|
||||
CopyString(owner_.client_id_with_password_,
|
||||
cache_v2_.client_id_with_password);
|
||||
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
||||
std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key));
|
||||
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
|
||||
LOG_INFO("Load settings from v2 cache file");
|
||||
if (loaded_v3) {
|
||||
LOG_INFO("Load settings from v3 cache file");
|
||||
} else {
|
||||
cache_v3_ = {};
|
||||
SaveLocked();
|
||||
LOG_INFO("Migrated settings from v2 to v3 cache file");
|
||||
}
|
||||
} else {
|
||||
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||
std::ios::binary);
|
||||
@@ -128,20 +282,14 @@ int SettingsManager::Load() {
|
||||
std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key));
|
||||
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||
|
||||
cache_v3_ = {};
|
||||
SaveLocked();
|
||||
LOG_INFO("Migrated settings from v1 to v2 cache file");
|
||||
LOG_INFO("Migrated settings from v1 to v3 cache file");
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
|
||||
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
|
||||
if (at_pos != nullptr) {
|
||||
const std::string id(owner_.client_id_with_password_,
|
||||
at_pos - owner_.client_id_with_password_);
|
||||
const std::string password(at_pos + 1);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, password.c_str());
|
||||
}
|
||||
ActivateCachedPublicIdentity();
|
||||
|
||||
owner_.thumbnail_ =
|
||||
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
|
||||
@@ -193,8 +341,11 @@ int SettingsManager::Load() {
|
||||
|
||||
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (!ReadV2Locked() || cache_v2_.self_hosted_id[0] == '\0') {
|
||||
if ((!ReadV3Locked() && !ReadV2Locked()) ||
|
||||
cache_v2_.self_hosted_id[0] == '\0') {
|
||||
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,27 +363,155 @@ bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsManager::ActivateCachedPublicIdentity() {
|
||||
if (owner_.client_id_with_password_[0] == '\0') {
|
||||
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
|
||||
if (at_pos == nullptr) {
|
||||
CopyString(owner_.client_id_, owner_.client_id_with_password_);
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
} else {
|
||||
const std::string id(owner_.client_id_with_password_,
|
||||
at_pos - owner_.client_id_with_password_);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, at_pos + 1);
|
||||
}
|
||||
return owner_.client_id_[0] != '\0';
|
||||
}
|
||||
|
||||
void SettingsManager::ActivateIdentity(const char *identity,
|
||||
bool self_hosted) {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self_hosted) {
|
||||
CopyString(owner_.self_hosted_id_, identity);
|
||||
} else {
|
||||
CopyString(owner_.client_id_with_password_, identity);
|
||||
}
|
||||
|
||||
const char *at_pos = std::strchr(identity, '@');
|
||||
if (at_pos == nullptr) {
|
||||
CopyString(owner_.client_id_, identity);
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string id(identity, at_pos - identity);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, at_pos + 1);
|
||||
}
|
||||
|
||||
bool SettingsManager::StagePendingPasswordChange(
|
||||
const std::string &identity, bool self_hosted,
|
||||
const std::string &server_host, int server_port,
|
||||
const std::string &request_id) {
|
||||
if (identity.empty() || identity.size() >= sizeof(cache_v3_.pending_identity) ||
|
||||
server_host.empty() ||
|
||||
server_host.size() >= sizeof(cache_v3_.pending_server_host) ||
|
||||
server_port <= 0 || request_id.empty() ||
|
||||
request_id.size() >= sizeof(cache_v3_.pending_request_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] != '\0' &&
|
||||
(cache_v3_.pending_self_hosted != self_hosted ||
|
||||
cache_v3_.pending_server_port != server_port ||
|
||||
server_host != cache_v3_.pending_server_host ||
|
||||
request_id != cache_v3_.pending_request_id)) {
|
||||
LOG_WARN("Refuse to overwrite an unresolved password change");
|
||||
return false;
|
||||
}
|
||||
const CacheV3 previous = cache_v3_;
|
||||
CopyString(cache_v3_.pending_identity, identity.c_str());
|
||||
CopyString(cache_v3_.pending_server_host, server_host.c_str());
|
||||
cache_v3_.pending_server_port = server_port;
|
||||
cache_v3_.pending_self_hosted = self_hosted;
|
||||
CopyString(cache_v3_.pending_request_id, request_id.c_str());
|
||||
if (SaveLocked() != 0) {
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SettingsManager::PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host, int server_port) const {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0' ||
|
||||
cache_v3_.pending_self_hosted != self_hosted ||
|
||||
cache_v3_.pending_server_port != server_port ||
|
||||
server_host != cache_v3_.pending_server_host) {
|
||||
return {};
|
||||
}
|
||||
return cache_v3_.pending_identity;
|
||||
}
|
||||
|
||||
bool SettingsManager::PromotePendingPasswordChange() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const CacheV3 previous = cache_v3_;
|
||||
const std::string identity = cache_v3_.pending_identity;
|
||||
const bool self_hosted = cache_v3_.pending_self_hosted;
|
||||
ActivateIdentity(identity.c_str(), self_hosted);
|
||||
std::memset(cache_v3_.pending_identity, 0,
|
||||
sizeof(cache_v3_.pending_identity));
|
||||
std::memset(cache_v3_.pending_server_host, 0,
|
||||
sizeof(cache_v3_.pending_server_host));
|
||||
cache_v3_.pending_server_port = 0;
|
||||
cache_v3_.pending_self_hosted = false;
|
||||
std::memset(cache_v3_.pending_request_id, 0,
|
||||
sizeof(cache_v3_.pending_request_id));
|
||||
if (SaveLocked() != 0) {
|
||||
// The already-durable pending credential remains the recovery source on
|
||||
// disk. Keep it in memory as well so a reconnect in this process retries
|
||||
// the credential that the server has accepted.
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsManager::ClearPendingPasswordChange() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const CacheV3 previous = cache_v3_;
|
||||
std::memset(cache_v3_.pending_identity, 0,
|
||||
sizeof(cache_v3_.pending_identity));
|
||||
std::memset(cache_v3_.pending_server_host, 0,
|
||||
sizeof(cache_v3_.pending_server_host));
|
||||
cache_v3_.pending_server_port = 0;
|
||||
cache_v3_.pending_self_hosted = false;
|
||||
std::memset(cache_v3_.pending_request_id, 0,
|
||||
sizeof(cache_v3_.pending_request_id));
|
||||
if (SaveLocked() != 0) {
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
|
||||
if (!client_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (!ReadV2Locked()) {
|
||||
cache_v2_ = {};
|
||||
}
|
||||
|
||||
CopyString(cache_v2_.self_hosted_id, client_id);
|
||||
CopyString(cache_v2_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
|
||||
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
if (cache_v2_file) {
|
||||
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
|
||||
sizeof(cache_v2_));
|
||||
CopyString(owner_.self_hosted_id_, client_id);
|
||||
if (SaveLocked() != 0) {
|
||||
LOG_ERROR("Failed to persist self-hosted identity atomically");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -33,8 +34,28 @@ public:
|
||||
// Loads the cached self-hosted identity into the owner's active connection
|
||||
// fields. Returns true only when a non-empty identity was restored.
|
||||
bool LoadCachedSelfHostedIdentity();
|
||||
// Restores the public-server identity kept in the in-memory cache fields.
|
||||
// This must run when switching away from a self-hosted server so signal
|
||||
// callbacks are matched against the identity used by the replacement peer.
|
||||
bool ActivateCachedPublicIdentity();
|
||||
void PersistSelfHostedIdentity(const char *client_id);
|
||||
|
||||
// Password rotation is deliberately persisted in two phases. The active
|
||||
// credential remains usable while the pending credential records the value
|
||||
// that may already have been committed by the server. On the next launch the
|
||||
// caller can try the pending credential first and safely fall back to the
|
||||
// active credential when the request never reached the server.
|
||||
bool StagePendingPasswordChange(const std::string &identity,
|
||||
bool self_hosted,
|
||||
const std::string &server_host,
|
||||
int server_port,
|
||||
const std::string &request_id);
|
||||
std::string PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host,
|
||||
int server_port) const;
|
||||
bool PromotePendingPasswordChange();
|
||||
bool ClearPendingPasswordChange();
|
||||
|
||||
private:
|
||||
struct CacheV1 {
|
||||
char client_id_with_password[17];
|
||||
@@ -63,12 +84,27 @@ private:
|
||||
char self_hosted_id[17];
|
||||
};
|
||||
|
||||
struct CacheV3 {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
CacheV2 base;
|
||||
char pending_identity[17];
|
||||
char pending_server_host[256];
|
||||
int pending_server_port;
|
||||
bool pending_self_hosted;
|
||||
char pending_request_id[64];
|
||||
uint32_t checksum;
|
||||
};
|
||||
|
||||
int SaveLocked();
|
||||
bool ReadV3Locked();
|
||||
bool ReadV2Locked();
|
||||
void ActivateIdentity(const char *identity, bool self_hosted);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
CacheV1 cache_v1_{};
|
||||
CacheV2 cache_v2_{};
|
||||
CacheV3 cache_v3_{};
|
||||
mutable std::mutex cache_mutex_;
|
||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
@@ -381,8 +382,10 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (const auto& display_info : devices_.display_info_list()) {
|
||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||
const auto& displays = devices_.display_info_list();
|
||||
for (size_t index = 0; index < displays.size(); ++index) {
|
||||
const std::string stream_id = MakeDisplayStreamId(index);
|
||||
AddVideoStream(props->peer_, stream_id.c_str());
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -57,9 +58,44 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
signal_server_ip = config_center_->GetDefaultServerHost();
|
||||
signal_server_port = config_center_->GetDefaultSignalServerPort();
|
||||
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
|
||||
settings_.ActivateCachedPublicIdentity();
|
||||
params_.user_id = client_id_with_password_;
|
||||
}
|
||||
|
||||
const bool self_hosted = config_center_->IsSelfHosted();
|
||||
const std::string pending_identity =
|
||||
settings_.PendingPasswordChangeIdentity(
|
||||
self_hosted, signal_server_ip, signal_server_port);
|
||||
bool try_pending_identity = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
if (pending_identity.empty()) {
|
||||
credential_recovery_in_progress_ = false;
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
credential_recovery_retry_active_ = false;
|
||||
credential_recovery_promote_pending_ = false;
|
||||
credential_recovery_clear_pending_ = false;
|
||||
} else if (!credential_recovery_in_progress_) {
|
||||
credential_recovery_in_progress_ = true;
|
||||
credential_recovery_attempt_pending_ = true;
|
||||
LOG_INFO("Recovering an interrupted password change for [{}]",
|
||||
client_id_);
|
||||
}
|
||||
try_pending_identity = credential_recovery_in_progress_ &&
|
||||
credential_recovery_attempt_pending_ &&
|
||||
!pending_identity.empty();
|
||||
}
|
||||
|
||||
const char *active_identity =
|
||||
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
|
||||
const std::string login_identity =
|
||||
try_pending_identity ? pending_identity : std::string(active_identity);
|
||||
std::memset(connection_login_identity_, 0,
|
||||
sizeof(connection_login_identity_));
|
||||
std::strncpy(connection_login_identity_, login_identity.c_str(),
|
||||
sizeof(connection_login_identity_) - 1);
|
||||
params_.user_id = connection_login_identity_;
|
||||
|
||||
// self hosted server config
|
||||
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
||||
sizeof(signal_server_ip_self_) - 1);
|
||||
@@ -94,12 +130,11 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.turn_server_ip) - 1);
|
||||
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
|
||||
params_.turn_server_port = coturn_server_port;
|
||||
strncpy((char *)params_.turn_server_username, "crossdesk",
|
||||
sizeof(params_.turn_server_username) - 1);
|
||||
params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0';
|
||||
strncpy((char *)params_.turn_server_password, "crossdeskpw",
|
||||
sizeof(params_.turn_server_password) - 1);
|
||||
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
|
||||
// TURN credentials are issued by the signaling server after login. Keep the
|
||||
// initial values empty so a reusable static password is never embedded in
|
||||
// the client binary.
|
||||
params_.turn_server_username[0] = '\0';
|
||||
params_.turn_server_password[0] = '\0';
|
||||
|
||||
strncpy(params_.log_path, dll_log_path_.c_str(),
|
||||
sizeof(params_.log_path) - 1);
|
||||
@@ -111,8 +146,14 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
: false;
|
||||
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
|
||||
params_.enable_srtp = config_center_->IsEnableSrtp();
|
||||
params_.video_content_type = VideoContentType::ScreenContent;
|
||||
params_.video_quality =
|
||||
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
||||
params_.video_frame_rate =
|
||||
config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
@@ -126,6 +167,11 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
|
||||
params_.user_data = &peer_events_;
|
||||
|
||||
// The previous peer may have left a terminal status behind. Reset it before
|
||||
// Init() starts emitting callbacks for the newly selected server.
|
||||
signal_connected_ = false;
|
||||
signal_status_ = SignalStatus::SignalConnecting;
|
||||
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
@@ -136,8 +182,10 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
}
|
||||
|
||||
if (0 == devices_.InitializeScreenCapturer()) {
|
||||
for (const auto &display_info : devices_.display_info_list()) {
|
||||
AddVideoStream(peer_, display_info.name.c_str());
|
||||
const auto &displays = devices_.display_info_list();
|
||||
for (size_t index = 0; index < displays.size(); ++index) {
|
||||
const std::string stream_id = MakeDisplayStreamId(index);
|
||||
AddVideoStream(peer_, stream_id.c_str());
|
||||
}
|
||||
|
||||
AddAudioStream(peer_, audio_label_.c_str());
|
||||
|
||||
@@ -26,6 +26,31 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
|
||||
const std::string& client_id) {
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
constexpr const char* kClientPlatform = "windows";
|
||||
#elif defined(__APPLE__)
|
||||
constexpr const char* kClientPlatform = "macos";
|
||||
#elif defined(__linux__)
|
||||
constexpr const char* kClientPlatform = "linux";
|
||||
#else
|
||||
constexpr const char* kClientPlatform = "unknown";
|
||||
#endif
|
||||
|
||||
const nlohmann::json message = {{"type", "client_info"},
|
||||
{"version", CROSSDESK_VERSION},
|
||||
{"platform", kClientPlatform}};
|
||||
const std::string payload = message.dump();
|
||||
if (SendSignalMessage(peer, payload.data(), payload.size()) != 0) {
|
||||
LOG_WARN("[{}] failed to report client information", client_id);
|
||||
}
|
||||
}
|
||||
|
||||
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
|
||||
|
||||
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
@@ -85,6 +110,39 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == "change_password") {
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (!runtime->password_change_pending_) {
|
||||
LOG_WARN("Ignore unexpected password change response");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!j.contains("request_id") || !j["request_id"].is_string() ||
|
||||
j["request_id"].get<std::string>() !=
|
||||
runtime->pending_password_change_request_id_) {
|
||||
LOG_WARN("Ignore password change response with unexpected request id");
|
||||
return;
|
||||
}
|
||||
|
||||
if (j.contains("user_id") && j["user_id"].is_string()) {
|
||||
const std::string response_user_id = j["user_id"].get<std::string>();
|
||||
if (!response_user_id.empty() &&
|
||||
response_user_id != runtime->client_id_) {
|
||||
LOG_WARN("Ignore password change response for unexpected id [{}]",
|
||||
response_user_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runtime->password_change_succeeded_ =
|
||||
j.contains("status") && j["status"].is_string() &&
|
||||
j["status"].get<std::string>() == "success";
|
||||
runtime->password_change_error_ =
|
||||
j.contains("reason") && j["reason"].is_string()
|
||||
? j["reason"].get<std::string>()
|
||||
: "Password change failed";
|
||||
runtime->password_change_result_uncertain_ = false;
|
||||
runtime->password_change_result_ready_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +163,22 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
runtime->signal_connected_ = true;
|
||||
runtime->need_to_send_recent_connections_ = true;
|
||||
LOG_INFO("[{}] connected to signal server", client_id);
|
||||
SendClientInfo(runtime->peer_, client_id);
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_) {
|
||||
if (runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_promote_pending_ = true;
|
||||
} else {
|
||||
runtime->credential_recovery_clear_pending_ = true;
|
||||
}
|
||||
}
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_ &&
|
||||
runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_retry_active_ = true;
|
||||
}
|
||||
} else if (SignalStatus::SignalClosed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalReconnecting == status) {
|
||||
@@ -134,6 +206,7 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
} else if (SignalStatus::SignalConnected == status) {
|
||||
props->signal_connected_ = true;
|
||||
LOG_INFO("[{}] connected to signal server", remote_id);
|
||||
SendClientInfo(props->peer_, client_id);
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalClosed == status) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
@@ -39,6 +40,8 @@ public:
|
||||
void *user_data);
|
||||
|
||||
private:
|
||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
};
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ struct UserSettingsState {
|
||||
char password_saved_[7] = "";
|
||||
char self_hosted_id_[17] = "";
|
||||
char self_hosted_user_id_[17] = "";
|
||||
char connection_login_identity_[17] = "";
|
||||
int language_button_value_ = 0;
|
||||
int video_quality_button_value_ = 2;
|
||||
int video_frame_rate_button_value_ = 1;
|
||||
@@ -152,6 +153,24 @@ struct UserSettingsState {
|
||||
bool show_file_browser_ = true;
|
||||
};
|
||||
|
||||
struct PasswordChangeState {
|
||||
std::mutex password_change_mutex_;
|
||||
uint64_t next_password_change_request_id_ = 0;
|
||||
bool password_change_pending_ = false;
|
||||
bool password_change_result_ready_ = false;
|
||||
bool password_change_succeeded_ = false;
|
||||
bool password_change_result_uncertain_ = false;
|
||||
std::chrono::steady_clock::time_point password_change_requested_at_;
|
||||
std::string pending_password_change_request_id_;
|
||||
std::string pending_local_password_;
|
||||
std::string password_change_error_;
|
||||
bool credential_recovery_in_progress_ = false;
|
||||
bool credential_recovery_attempt_pending_ = false;
|
||||
bool credential_recovery_retry_active_ = false;
|
||||
bool credential_recovery_promote_pending_ = false;
|
||||
bool credential_recovery_clear_pending_ = false;
|
||||
};
|
||||
|
||||
struct ConnectionState {
|
||||
using RemoteSessionMap =
|
||||
std::unordered_map<std::string, RemoteSessionPtr>;
|
||||
@@ -180,6 +199,7 @@ struct RuntimeState : InfrastructureState,
|
||||
PeerState,
|
||||
PlatformIntegrationState,
|
||||
UserSettingsState,
|
||||
PasswordChangeState,
|
||||
ConnectionState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
+134
-15
@@ -167,6 +167,8 @@ export component StreamWindow inherits Window {
|
||||
|
||||
private property <bool> control-expanded: true;
|
||||
private property <bool> display-menu-open: false;
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
private property <bool> control-docked-left: true;
|
||||
private property <bool> control-dragging: false;
|
||||
@@ -183,6 +185,48 @@ export component StreamWindow inherits Window {
|
||||
callback begin-control-drag(length, length);
|
||||
callback move-control-drag(length, length);
|
||||
callback end-control-drag;
|
||||
pure callback is-local-control-area(length, length) -> bool;
|
||||
|
||||
animate display-menu-progress {
|
||||
duration: 120ms;
|
||||
easing: ease-out;
|
||||
enabled: root.display-menu-animation-enabled;
|
||||
}
|
||||
|
||||
display-menu-open-timer := Timer {
|
||||
interval: 16ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
if root.display-menu-open {
|
||||
root.display-menu-animation-enabled = true;
|
||||
root.display-menu-progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is-local-control-area(pointer-x, pointer-y) => {
|
||||
let over-control = pointer-x >= control.x
|
||||
&& pointer-x <= control.x + control.width
|
||||
&& pointer-y >= control.y
|
||||
&& pointer-y <= control.y + control.height;
|
||||
let display-menu-x = control.x + (root.control-docked-left ? 9px : 42px);
|
||||
let display-menu-y = control.y + control.height - 1px;
|
||||
let display-menu-height = min(180px, root.displays.length * 30px + 8px);
|
||||
let over-display-menu = root.display-menu-open
|
||||
&& pointer-x >= display-menu-x
|
||||
&& pointer-x <= display-menu-x + 180px
|
||||
&& pointer-y >= display-menu-y
|
||||
&& pointer-y <= display-menu-y + display-menu-height;
|
||||
let shortcut-menu-x = control.x + (root.control-docked-left ? 41px : 74px);
|
||||
let shortcut-menu-y = control.y + 40px;
|
||||
let over-shortcut-menu = root.shortcut-menu-open
|
||||
&& pointer-x >= shortcut-menu-x
|
||||
&& pointer-x <= shortcut-menu-x + 150px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
return over-control || over-display-menu || over-shortcut-menu;
|
||||
}
|
||||
|
||||
begin-control-drag(press-x, press-y) => {
|
||||
// Capture the snapped position before switching x to control-drag-x.
|
||||
@@ -195,6 +239,9 @@ export component StreamWindow inherits Window {
|
||||
// pointer positions remain relative to the stationary video surface.
|
||||
root.control-press-x = press-x - current-x;
|
||||
root.control-press-y = press-y - current-y;
|
||||
display-menu-open-timer.running = false;
|
||||
root.display-menu-animation-enabled = false;
|
||||
root.display-menu-progress = 0.0;
|
||||
root.display-menu-open = false;
|
||||
root.shortcut-menu-open = false;
|
||||
root.control-dragging = true;
|
||||
@@ -503,7 +550,9 @@ export component StreamWindow inherits Window {
|
||||
&& self.mouse-x <= control.x + control.width
|
||||
&& self.mouse-y >= control.y
|
||||
&& self.mouse-y <= control.y + control.height;
|
||||
if event.kind == PointerEventKind.down && !over-control {
|
||||
let over-local-ui = root.is-local-control-area(
|
||||
self.mouse-x, self.mouse-y);
|
||||
if event.kind == PointerEventKind.down && !over-local-ui {
|
||||
input-focus.focus();
|
||||
}
|
||||
if event.kind == PointerEventKind.down
|
||||
@@ -515,9 +564,9 @@ export component StreamWindow inherits Window {
|
||||
|| event.kind == PointerEventKind.cancel) {
|
||||
root.end-control-drag();
|
||||
// The control bar overlays the remote video. Events in
|
||||
// its full rectangle (including gaps between buttons)
|
||||
// it and its attached menus (including padding and gaps)
|
||||
// are local UI input and must never reach the peer.
|
||||
} else if !root.control-dragging && !over-control {
|
||||
} else if !root.control-dragging && !over-local-ui {
|
||||
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||
}
|
||||
}
|
||||
@@ -527,10 +576,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
scroll-event(event) => {
|
||||
if self.mouse-x < control.x
|
||||
|| self.mouse-x > control.x + control.width
|
||||
|| self.mouse-y < control.y
|
||||
|| self.mouse-y > control.y + control.height {
|
||||
if !root.is-local-control-area(self.mouse-x, self.mouse-y) {
|
||||
root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y);
|
||||
}
|
||||
accept
|
||||
@@ -565,12 +611,27 @@ export component StreamWindow inherits Window {
|
||||
icon: FontAwesomeIcons.display;
|
||||
badge: root.selected-display + 1;
|
||||
tooltip: StreamStrings.select-display;
|
||||
clicked => { root.display-menu-open = !root.display-menu-open; root.shortcut-menu-open = false; }
|
||||
clicked => {
|
||||
display-menu-open-timer.running = false;
|
||||
root.display-menu-animation-enabled = false;
|
||||
root.display-menu-progress = 0.0;
|
||||
root.display-menu-open = !root.display-menu-open;
|
||||
root.shortcut-menu-open = false;
|
||||
if root.display-menu-open {
|
||||
display-menu-open-timer.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.control-expanded: shortcut-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 41px : 74px; y: 7px;
|
||||
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
|
||||
clicked => { root.shortcut-menu-open = !root.shortcut-menu-open; root.display-menu-open = false; }
|
||||
clicked => {
|
||||
display-menu-open-timer.running = false;
|
||||
root.display-menu-animation-enabled = false;
|
||||
root.display-menu-progress = 0.0;
|
||||
root.shortcut-menu-open = !root.shortcut-menu-open;
|
||||
root.display-menu-open = false;
|
||||
}
|
||||
}
|
||||
if root.control-expanded: mouse-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 73px : 106px; y: 7px;
|
||||
@@ -753,18 +814,76 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
if root.display-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 9px : 42px); y: control.y + 40px; width: 180px; height: min(180px, root.displays.length * 30px + 8px);
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
if root.display-menu-open: display-menu := Rectangle {
|
||||
private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
|
||||
private property <int> hovered-index:
|
||||
display-touch.has-hover && display-touch.mouse-y >= 4px
|
||||
&& floor((display-touch.mouse-y - 4px) / 30px) < root.displays.length
|
||||
? floor((display-touch.mouse-y - 4px) / 30px) : -1;
|
||||
x: control.x + (root.control-docked-left ? 9px : 42px);
|
||||
// Overlap the control border by one pixel so the menu reads as
|
||||
// an attached panel growing downward from the display button.
|
||||
y: control.y + control.height - 1px;
|
||||
width: 180px;
|
||||
height: self.expanded-height * root.display-menu-progress;
|
||||
opacity: root.display-menu-progress;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 5px;
|
||||
clip: true;
|
||||
z: 11;
|
||||
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
for display[index] in root.displays: Rectangle {
|
||||
height: 29px;
|
||||
background: display-touch.has-hover || index == root.selected-display ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: display; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
display-touch := TouchArea { clicked => { root.selected-display = index; root.switch-display(index); root.display-menu-open = false; } }
|
||||
background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2
|
||||
: index == display-menu.hovered-index ? #d8e6f8
|
||||
: index == root.selected-display ? #e8eef9
|
||||
: transparent;
|
||||
border-radius: 3px;
|
||||
Text {
|
||||
x: 8px;
|
||||
width: parent.width - 34px;
|
||||
text: display;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if index == root.selected-display: Text {
|
||||
x: parent.width - 24px;
|
||||
width: 16px;
|
||||
text: FontAwesomeIcons.check;
|
||||
color: #2463c7;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use one hit target for the whole popup. A single owner keeps
|
||||
// hover stable while still consuming padding, gaps and scroll
|
||||
// events locally instead of forwarding them to the peer.
|
||||
display-touch := TouchArea {
|
||||
clicked => {
|
||||
let index = floor((self.mouse-y - 4px) / 30px);
|
||||
let row-y = self.mouse-y - 4px - index * 30px;
|
||||
if self.mouse-y >= 4px && row-y < 29px
|
||||
&& index >= 0 && index < root.displays.length {
|
||||
display-menu-open-timer.running = false;
|
||||
root.display-menu-animation-enabled = false;
|
||||
root.display-menu-progress = 0.0;
|
||||
root.selected-display = index;
|
||||
root.switch-display(index);
|
||||
root.display-menu-open = false;
|
||||
}
|
||||
}
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
|
||||
@@ -108,7 +108,10 @@ int GuiApplication::ControlBar(
|
||||
if (ImGui::BeginPopup("display")) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < props->display_info_list_.size(); i++) {
|
||||
if (ImGui::Selectable(props->display_info_list_[i].name.c_str())) {
|
||||
const std::string display_label = localization::FormatDisplayLabel(
|
||||
static_cast<size_t>(i), props->display_info_list_[i].name,
|
||||
localization_language_index_);
|
||||
if (ImGui::Selectable(display_label.c_str())) {
|
||||
props->selected_display_ = i;
|
||||
|
||||
RemoteAction remote_action;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -262,6 +263,7 @@ bool ScreenCapturerDrm::DiscoverOutputs() {
|
||||
output.height = static_cast<int>(crtc->height);
|
||||
output.name = std::string(ConnectorTypeName(connector->connector_type)) +
|
||||
std::to_string(connector->connector_type_id);
|
||||
output.stream_id = MakeDisplayStreamId(outputs_.size());
|
||||
|
||||
outputs_.push_back(output);
|
||||
display_info_list_.push_back(
|
||||
@@ -427,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
|
||||
|
||||
if (emit_callback && callback_) {
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
|
||||
capture_height, output.name.c_str());
|
||||
capture_height, output.stream_id.c_str());
|
||||
}
|
||||
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
|
||||
@@ -47,6 +47,7 @@ class ScreenCapturerDrm : public ScreenCapturer {
|
||||
uint32_t connector_id = 0;
|
||||
uint32_t crtc_id = 0;
|
||||
std::string name;
|
||||
std::string stream_id;
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
int width = 0;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
@@ -49,10 +50,21 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
callback_orig_ = std::move(cb);
|
||||
callback_ = [this](unsigned char* data, int size, int width, int height,
|
||||
const char* display_name) {
|
||||
const std::string mapped_name = MapDisplayName(display_name);
|
||||
const char* reported_stream_id) {
|
||||
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
LOG_WARN("Linux capturer dropping frame without a registered stream "
|
||||
"id: reported='{}', size={}x{}, bytes={}",
|
||||
reported_stream_id ? reported_stream_id : "", width, height,
|
||||
size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
if (callback_orig_) {
|
||||
callback_orig_(data, size, width, height, mapped_name.c_str());
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,10 +151,12 @@ int ScreenCapturerLinux::Destroy() {
|
||||
backend_ = BackendType::kNone;
|
||||
callback_ = nullptr;
|
||||
callback_orig_ = nullptr;
|
||||
current_monitor_index_.store(0, std::memory_order_relaxed);
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
canonical_displays_.clear();
|
||||
label_alias_.clear();
|
||||
stream_id_alias_.clear();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -153,15 +167,12 @@ int ScreenCapturerLinux::Start(bool show_cursor) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (backend_ == BackendType::kWayland) {
|
||||
const int refresh_ret = RefreshWaylandBackend();
|
||||
if (refresh_ret != 0) {
|
||||
LOG_WARN("Linux screen capturer Wayland backend refresh failed: {}",
|
||||
refresh_ret);
|
||||
}
|
||||
// X11 output names, DRM connectors and Wayland stream handles may all change
|
||||
// after an HDMI hotplug, so rebuild the current backend before each session.
|
||||
const int refresh_ret = RefreshCurrentBackend();
|
||||
if (refresh_ret != 0) {
|
||||
LOG_WARN("Linux screen capturer backend refresh failed: {}", refresh_ret);
|
||||
}
|
||||
#endif
|
||||
|
||||
const int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) {
|
||||
@@ -235,14 +246,23 @@ int ScreenCapturerLinux::SwitchTo(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
return impl_->SwitchTo(monitor_index);
|
||||
const int ret = impl_->SwitchTo(monitor_index);
|
||||
if (ret == 0) {
|
||||
current_monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::ResetToInitialMonitor() {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
return impl_->ResetToInitialMonitor();
|
||||
const int ret = impl_->ResetToInitialMonitor();
|
||||
if (ret == 0) {
|
||||
current_monitor_index_.store(initial_monitor_index_,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
|
||||
@@ -320,27 +340,55 @@ int ScreenCapturerLinux::InitWayland() {
|
||||
#endif
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::RefreshWaylandBackend() {
|
||||
int ScreenCapturerLinux::RefreshCurrentBackend() {
|
||||
std::unique_ptr<ScreenCapturer> backend;
|
||||
const char* backend_name = "unknown";
|
||||
switch (backend_) {
|
||||
case BackendType::kX11:
|
||||
backend = std::make_unique<ScreenCapturerX11>();
|
||||
backend_name = "X11";
|
||||
break;
|
||||
case BackendType::kDrm:
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
backend = std::make_unique<ScreenCapturerDrm>();
|
||||
backend_name = "DRM";
|
||||
break;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
case BackendType::kWayland:
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
auto backend = std::make_unique<ScreenCapturerWayland>();
|
||||
backend = std::make_unique<ScreenCapturerWayland>();
|
||||
backend_name = "Wayland";
|
||||
break;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
case BackendType::kNone:
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int ret = backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
backend->Destroy();
|
||||
return ret;
|
||||
}
|
||||
|
||||
const int requested_monitor =
|
||||
current_monitor_index_.load(std::memory_order_relaxed);
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
if (requested_monitor > 0 && backend->SwitchTo(requested_monitor) != 0) {
|
||||
current_monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
impl_ = std::move(backend);
|
||||
backend_ = BackendType::kWayland;
|
||||
LOG_INFO("Linux screen capturer Wayland backend refreshed before start");
|
||||
LOG_INFO("Linux screen capturer {} backend refreshed before start",
|
||||
backend_name);
|
||||
return 0;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
|
||||
@@ -447,61 +495,82 @@ void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
label_alias_.clear();
|
||||
stream_id_alias_.clear();
|
||||
|
||||
if (canonical_displays_.empty()) {
|
||||
canonical_displays_ = backend_displays;
|
||||
for (const auto& display : backend_displays) {
|
||||
label_alias_[display.name] = display.name;
|
||||
for (size_t i = 0; i < backend_displays.size(); ++i) {
|
||||
auto& canonical = canonical_displays_[i];
|
||||
const std::string stream_id = MakeDisplayStreamId(i);
|
||||
if (canonical.name.empty()) {
|
||||
canonical.name = stream_id;
|
||||
}
|
||||
stream_id_alias_[stream_id] = stream_id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (canonical_displays_.size() < backend_displays.size()) {
|
||||
for (size_t i = canonical_displays_.size(); i < backend_displays.size();
|
||||
++i) {
|
||||
canonical_displays_.push_back(backend_displays[i]);
|
||||
}
|
||||
LOG_WARN("Linux capturer detected {} additional display(s); they remain "
|
||||
"unavailable until peer streams are reinitialized",
|
||||
backend_displays.size() - canonical_displays_.size());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < backend_displays.size(); ++i) {
|
||||
const std::string mapped_name = i < canonical_displays_.size()
|
||||
? canonical_displays_[i].name
|
||||
: backend_displays[i].name;
|
||||
label_alias_[backend_displays[i].name] = mapped_name;
|
||||
auto similar = [](const DisplayInfo& current, const DisplayInfo& canonical) {
|
||||
return std::abs(current.left - canonical.left) <= 10 &&
|
||||
std::abs(current.top - canonical.top) <= 10 &&
|
||||
std::abs(current.width - canonical.width) <= 20 &&
|
||||
std::abs(current.height - canonical.height) <= 20;
|
||||
};
|
||||
std::vector<bool> used(canonical_displays_.size(), false);
|
||||
for (size_t current_index = 0; current_index < backend_displays.size();
|
||||
++current_index) {
|
||||
const auto& backend_display = backend_displays[current_index];
|
||||
int canonical_index = -1;
|
||||
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
|
||||
if (!used[i] && similar(backend_display, canonical_displays_[i])) {
|
||||
canonical_index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canonical_index < 0 && current_index < canonical_displays_.size() &&
|
||||
!used[current_index]) {
|
||||
canonical_index = static_cast<int>(current_index);
|
||||
}
|
||||
if (canonical_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i < canonical_displays_.size()) {
|
||||
// Keep original stable names, but refresh geometry from active backend.
|
||||
canonical_displays_[i].handle = backend_displays[i].handle;
|
||||
canonical_displays_[i].is_primary = backend_displays[i].is_primary;
|
||||
canonical_displays_[i].left = backend_displays[i].left;
|
||||
canonical_displays_[i].top = backend_displays[i].top;
|
||||
canonical_displays_[i].right = backend_displays[i].right;
|
||||
canonical_displays_[i].bottom = backend_displays[i].bottom;
|
||||
canonical_displays_[i].width = backend_displays[i].width;
|
||||
canonical_displays_[i].height = backend_displays[i].height;
|
||||
used[canonical_index] = true;
|
||||
const std::string backend_stream_id =
|
||||
MakeDisplayStreamId(current_index);
|
||||
const std::string stable_stream_id =
|
||||
MakeDisplayStreamId(static_cast<size_t>(canonical_index));
|
||||
stream_id_alias_[backend_stream_id] = stable_stream_id;
|
||||
|
||||
// Refresh the user-visible label and geometry. The MiniRTC stream ID is
|
||||
// derived independently from this stable logical index.
|
||||
canonical_displays_[canonical_index] = backend_display;
|
||||
if (canonical_displays_[canonical_index].name.empty()) {
|
||||
canonical_displays_[canonical_index].name = stable_stream_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ScreenCapturerLinux::MapDisplayName(
|
||||
const char* display_name) const {
|
||||
std::string input_name = display_name ? display_name : "";
|
||||
if (input_name.empty()) {
|
||||
return input_name;
|
||||
}
|
||||
|
||||
std::string ScreenCapturerLinux::MapStreamId(
|
||||
const char* reported_stream_id) const {
|
||||
std::string input_id = reported_stream_id ? reported_stream_id : "";
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = label_alias_.find(input_name);
|
||||
if (it != label_alias_.end()) {
|
||||
return it->second;
|
||||
auto it = stream_id_alias_.find(input_id);
|
||||
if (it != stream_id_alias_.end()) {
|
||||
input_id = it->second;
|
||||
} else {
|
||||
input_id.clear();
|
||||
}
|
||||
|
||||
if (canonical_displays_.size() == 1) {
|
||||
return canonical_displays_[0].name;
|
||||
}
|
||||
|
||||
return input_name;
|
||||
return ResolveDisplayStreamId(
|
||||
input_id.c_str(), canonical_displays_.size(),
|
||||
current_monitor_index_.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef _SCREEN_CAPTURER_LINUX_H_
|
||||
#define _SCREEN_CAPTURER_LINUX_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -43,12 +44,12 @@ class ScreenCapturerLinux : public ScreenCapturer {
|
||||
int InitX11();
|
||||
int InitDrm();
|
||||
int InitWayland();
|
||||
int RefreshWaylandBackend();
|
||||
int RefreshCurrentBackend();
|
||||
bool TryFallbackToDrm(bool show_cursor);
|
||||
bool TryFallbackToX11(bool show_cursor);
|
||||
bool TryFallbackToWayland(bool show_cursor);
|
||||
void UpdateAliasesFromBackend(ScreenCapturer* backend);
|
||||
std::string MapDisplayName(const char* display_name) const;
|
||||
std::string MapStreamId(const char* reported_stream_id) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
@@ -58,7 +59,10 @@ class ScreenCapturerLinux : public ScreenCapturer {
|
||||
cb_desktop_data callback_orig_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
mutable std::mutex alias_mutex_;
|
||||
std::unordered_map<std::string, std::string> label_alias_;
|
||||
std::unordered_map<std::string, std::string> stream_id_alias_;
|
||||
std::atomic<int> current_monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
mutable std::atomic<bool> invalid_stream_id_logged_{false};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -831,8 +832,9 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
||||
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
|
||||
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(0);
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
|
||||
even_height, display_name_.c_str());
|
||||
even_height, stream_id.c_str());
|
||||
}
|
||||
pipewire_last_frame_ms_.store(NowMs());
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -97,12 +98,6 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
|
||||
name = "Display" + std::to_string(i + 1);
|
||||
}
|
||||
|
||||
// clean display name, remove non-alphanumeric characters
|
||||
name.erase(
|
||||
std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
|
||||
display_info_list_.push_back(DisplayInfo(
|
||||
(void*)display_, name, true, crtc_info->x, crtc_info->y,
|
||||
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height));
|
||||
@@ -324,8 +319,9 @@ void ScreenCapturerX11::OnFrame() {
|
||||
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
|
||||
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(monitor_index);
|
||||
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
|
||||
display_info_list_[monitor_index].name.c_str());
|
||||
stream_id.c_str());
|
||||
}
|
||||
|
||||
XDestroyImage(image);
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
|
||||
#include "screen_capturer_sck.h"
|
||||
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <IOKit/graphics/IOGraphicsLib.h>
|
||||
#include <IOSurface/IOSurface.h>
|
||||
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "display_info.h"
|
||||
#include "display_stream_id.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
using namespace crossdesk;
|
||||
@@ -83,20 +83,23 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
|
||||
int Resume(int monitor_index) override { return 0; }
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
return display_info_list_;
|
||||
}
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::map<int, CGDirectDisplayID> display_id_map_;
|
||||
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
|
||||
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
|
||||
unsigned char *nv12_frame_ = nullptr;
|
||||
size_t nv12_frame_size_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 60;
|
||||
bool show_cursor_ = false;
|
||||
bool capture_requested_ = false;
|
||||
bool invalid_stream_id_logged_ = false;
|
||||
|
||||
public:
|
||||
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
|
||||
@@ -130,9 +133,38 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// support full-desktop capture, and will fall back to the first display.
|
||||
CGDirectDisplayID current_display_ = 0;
|
||||
int initial_monitor_index_ = 0;
|
||||
int current_monitor_index_ = 0;
|
||||
std::string current_stream_id_;
|
||||
};
|
||||
|
||||
static std::string NSStringToUtf8(NSString *value) {
|
||||
if (!value || value.length == 0) return "";
|
||||
const char *utf8 = value.UTF8String;
|
||||
return utf8 ? utf8 : "";
|
||||
}
|
||||
|
||||
static std::string GetAppKitDisplayName(CGDirectDisplayID display_id) {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
@autoreleasepool {
|
||||
for (NSScreen *screen in NSScreen.screens) {
|
||||
NSNumber *screen_number = screen.deviceDescription[@"NSScreenNumber"];
|
||||
if (screen_number && screen_number.unsignedIntValue == display_id) {
|
||||
return NSStringToUtf8(screen.localizedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string GetDisplayName(CGDirectDisplayID display_id) {
|
||||
// IODisplayConnect is no longer exposed for some external displays on
|
||||
// modern Apple Silicon Macs. NSScreen is the system-owned source for the
|
||||
// user-visible name and maps directly to CGDirectDisplayID.
|
||||
std::string appkit_name = GetAppKitDisplayName(display_id);
|
||||
if (!appkit_name.empty()) return appkit_name;
|
||||
|
||||
// Keep the IOKit path for older macOS versions and display drivers.
|
||||
io_iterator_t iter;
|
||||
io_service_t serv = 0, matched_serv = 0;
|
||||
|
||||
@@ -212,8 +244,6 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
display_id_name_map_.clear();
|
||||
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
@@ -232,8 +262,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps > 0 ? fps : 60;
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
display_id_name_map_.clear();
|
||||
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||
@@ -265,26 +293,18 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID displays[10];
|
||||
uint32_t count;
|
||||
CGGetActiveDisplayList(10, displays, &count);
|
||||
|
||||
int unnamed_count = 1;
|
||||
for (SCDisplay *display in content.displays) {
|
||||
CGDirectDisplayID display_id = display.displayID;
|
||||
CGRect bounds = CGDisplayBounds(display_id);
|
||||
bool is_primary = CGDisplayIsMain(display_id);
|
||||
|
||||
std::string name = GetDisplayName(display_id);
|
||||
|
||||
if (name.empty()) {
|
||||
name = "Display" + std::to_string(unnamed_count++);
|
||||
name = MakeDisplayStreamId(display_info_list_.size());
|
||||
}
|
||||
|
||||
// clean display name, remove non-alphanumeric characters
|
||||
name.erase(
|
||||
std::remove_if(name.begin(), name.end(), [](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
LOG_INFO("macOS display discovered: index={}, display_id={}, name='{}'",
|
||||
display_info_list_.size(), display_id, name);
|
||||
|
||||
DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary,
|
||||
static_cast<int>(bounds.origin.x), static_cast<int>(bounds.origin.y),
|
||||
@@ -293,11 +313,12 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
display_info_list_.push_back(info);
|
||||
display_id_map_[display_info_list_.size() - 1] = display_id;
|
||||
display_id_map_reverse_[display_id] = display_info_list_.size() - 1;
|
||||
display_id_name_map_[display_id] = name;
|
||||
}
|
||||
|
||||
initial_monitor_index_ = 0;
|
||||
current_monitor_index_ = initial_monitor_index_;
|
||||
current_display_ = display_id_map_[current_monitor_index_];
|
||||
current_stream_id_ = MakeDisplayStreamId(current_monitor_index_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -307,49 +328,69 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("Cannot start capturer: display info not initialized");
|
||||
return -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("Cannot start capturer: display info not initialized");
|
||||
return -1;
|
||||
}
|
||||
show_cursor_ = show_cursor;
|
||||
capture_requested_ = true;
|
||||
invalid_stream_id_logged_ = false;
|
||||
}
|
||||
|
||||
show_cursor_ = show_cursor;
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||
auto display_it = display_id_map_.find(monitor_index);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||
monitor_index, display_id_map_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
const CGDirectDisplayID target_display = display_it->second;
|
||||
bool should_reconfigure = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
current_display_ = target_display;
|
||||
auto display_it = display_id_map_.find(monitor_index);
|
||||
if (display_it == display_id_map_.end() || monitor_index < 0 ||
|
||||
monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||
monitor_index, display_id_map_.size());
|
||||
return -1;
|
||||
}
|
||||
current_monitor_index_ = monitor_index;
|
||||
current_display_ = display_it->second;
|
||||
current_stream_id_ = MakeDisplayStreamId(monitor_index);
|
||||
should_reconfigure = capture_requested_;
|
||||
}
|
||||
if (should_reconfigure) {
|
||||
StartOrReconfigureCapturer();
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||
int target = initial_monitor_index_;
|
||||
if (display_info_list_.empty()) return -1;
|
||||
auto display_it = display_id_map_.find(target);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID target_display = display_it->second;
|
||||
if (current_display_ == target_display) return 0;
|
||||
const int target = initial_monitor_index_;
|
||||
bool should_reconfigure = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (display_info_list_.empty()) return -1;
|
||||
auto display_it = display_id_map_.find(target);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||
return -1;
|
||||
}
|
||||
const CGDirectDisplayID target_display = display_it->second;
|
||||
if (current_display_ == target_display &&
|
||||
current_monitor_index_ == target) {
|
||||
return 0;
|
||||
}
|
||||
current_monitor_index_ = target;
|
||||
current_display_ = target_display;
|
||||
current_stream_id_ = MakeDisplayStreamId(target);
|
||||
should_reconfigure = capture_requested_ && stream_ != nil;
|
||||
}
|
||||
|
||||
// Resetting session state must not create a capture stream. Preserve the
|
||||
// selected monitor for the next Start(), and only reconfigure an active one.
|
||||
if (should_reconfigure) {
|
||||
StartOrReconfigureCapturer();
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -362,7 +403,9 @@ int ScreenCapturerSckImpl::Destroy() {
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
}
|
||||
capture_requested_ = false;
|
||||
current_display_ = 0;
|
||||
current_stream_id_.clear();
|
||||
permanent_error_ = false;
|
||||
_on_data = nullptr;
|
||||
helper_to_release = helper_;
|
||||
@@ -376,6 +419,7 @@ int ScreenCapturerSckImpl::Destroy() {
|
||||
|
||||
int ScreenCapturerSckImpl::Stop() {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
capture_requested_ = false;
|
||||
if (stream_) {
|
||||
LOG_INFO("Stopping stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
@@ -387,6 +431,14 @@ int ScreenCapturerSckImpl::Stop() {
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *content) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_) {
|
||||
LOG_INFO("Ignoring stale ScreenCaptureKit display refresh after stop");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
LOG_ERROR("getShareableContent failed");
|
||||
permanent_error_ = true;
|
||||
@@ -400,22 +452,71 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
}
|
||||
|
||||
SCDisplay *captured_display = nil;
|
||||
bool show_cursor = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_) return;
|
||||
|
||||
int logical_index = current_monitor_index_;
|
||||
if (logical_index < 0 ||
|
||||
logical_index >= static_cast<int>(display_info_list_.size())) {
|
||||
logical_index = 0;
|
||||
}
|
||||
|
||||
SCDisplay *logical_fallback = nil;
|
||||
int display_index = 0;
|
||||
for (SCDisplay *display in content.displays) {
|
||||
if (display_index == logical_index) logical_fallback = display;
|
||||
if (current_display_ != 0 && current_display_ == display.displayID) {
|
||||
LOG_WARN("current display: {}, name: {}", current_display_,
|
||||
display_id_name_map_[current_display_]);
|
||||
captured_display = display;
|
||||
break;
|
||||
}
|
||||
++display_index;
|
||||
}
|
||||
if (!captured_display) {
|
||||
captured_display = content.displays.firstObject;
|
||||
if (captured_display) {
|
||||
current_display_ = captured_display.displayID;
|
||||
captured_display = logical_fallback ? logical_fallback
|
||||
: content.displays.firstObject;
|
||||
if (!logical_fallback) logical_index = 0;
|
||||
}
|
||||
|
||||
if (captured_display) {
|
||||
const CGDirectDisplayID old_display = current_display_;
|
||||
const CGDirectDisplayID new_display = captured_display.displayID;
|
||||
const std::string stable_stream_id = ResolveDisplayStreamId(
|
||||
current_stream_id_.c_str(), display_info_list_.size(), logical_index);
|
||||
if (stable_stream_id.empty()) {
|
||||
LOG_ERROR("Cannot map macOS display {} to a registered video stream",
|
||||
new_display);
|
||||
return;
|
||||
}
|
||||
|
||||
current_monitor_index_ = logical_index;
|
||||
current_display_ = new_display;
|
||||
current_stream_id_ = stable_stream_id;
|
||||
display_id_map_[logical_index] = new_display;
|
||||
|
||||
CGRect bounds = CGDisplayBounds(new_display);
|
||||
auto& info = display_info_list_[logical_index];
|
||||
info.handle = (void *)(uintptr_t)new_display;
|
||||
info.is_primary = CGDisplayIsMain(new_display);
|
||||
info.left = static_cast<int>(bounds.origin.x);
|
||||
info.top = static_cast<int>(bounds.origin.y);
|
||||
info.right = static_cast<int>(bounds.origin.x + bounds.size.width);
|
||||
info.bottom = static_cast<int>(bounds.origin.y + bounds.size.height);
|
||||
info.width = info.right - info.left;
|
||||
info.height = info.bottom - info.top;
|
||||
const std::string refreshed_name = GetDisplayName(new_display);
|
||||
if (!refreshed_name.empty()) {
|
||||
info.name = refreshed_name;
|
||||
}
|
||||
|
||||
if (old_display != new_display) {
|
||||
LOG_INFO("macOS display mapping refreshed: slot={}, old_id={}, "
|
||||
"new_id={}, stream='{}'",
|
||||
logical_index, old_display, new_display, stable_stream_id);
|
||||
}
|
||||
}
|
||||
show_cursor = show_cursor_;
|
||||
}
|
||||
|
||||
if (!captured_display) {
|
||||
@@ -440,13 +541,14 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
}
|
||||
|
||||
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
|
||||
config.showsCursor = show_cursor_;
|
||||
config.showsCursor = show_cursor;
|
||||
config.width = filter.contentRect.size.width * filter.pointPixelScale;
|
||||
config.height = filter.contentRect.size.height * filter.pointPixelScale;
|
||||
config.captureResolution = SCCaptureResolutionAutomatic;
|
||||
config.minimumFrameInterval = CMTimeMake(1, fps_);
|
||||
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_) return;
|
||||
|
||||
if (stream_) {
|
||||
LOG_INFO("Updating stream configuration");
|
||||
@@ -516,7 +618,7 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data) {
|
||||
if (!_on_data || !capture_requested_) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
@@ -551,9 +653,22 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||
}
|
||||
|
||||
const std::string stream_id = ResolveDisplayStreamId(
|
||||
current_stream_id_.c_str(), display_info_list_.size(),
|
||||
current_monitor_index_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_) {
|
||||
LOG_ERROR("Dropping macOS frames without a registered stream id, "
|
||||
"display_id={}",
|
||||
current_display_);
|
||||
invalid_stream_id_logged_ = true;
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_ = false;
|
||||
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
display_id_name_map_[current_display_].c_str());
|
||||
static_cast<int>(height), stream_id.c_str());
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace crossdesk {
|
||||
|
||||
class ScreenCapturer {
|
||||
public:
|
||||
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
|
||||
// not a platform display name or physical handle.
|
||||
typedef std::function<void(unsigned char*, int, int, int, const char*)>
|
||||
cb_desktop_data;
|
||||
|
||||
@@ -34,4 +36,4 @@ class ScreenCapturer {
|
||||
virtual int ResetToInitialMonitor() = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string CleanDisplayName(const std::wstring& wide_name) {
|
||||
std::string GetDisplayLabel(const std::wstring& wide_name) {
|
||||
std::string name = WideToUtf8(wide_name);
|
||||
name.erase(std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
constexpr char kDevicePrefix[] = "\\\\.\\";
|
||||
if (name.rfind(kDevicePrefix, 0) == 0) {
|
||||
name.erase(0, sizeof(kDevicePrefix) - 1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
@@ -207,7 +209,7 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
|
||||
if (FAILED(output->GetDesc(&desc))) {
|
||||
continue;
|
||||
}
|
||||
std::string name = CleanDisplayName(desc.DeviceName);
|
||||
std::string name = GetDisplayLabel(desc.DeviceName);
|
||||
MONITORINFOEX mi{};
|
||||
mi.cbSize = sizeof(MONITORINFOEX);
|
||||
if (GetMonitorInfo(desc.Monitor, &mi)) {
|
||||
@@ -380,8 +382,9 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
if (callback_) {
|
||||
int idx = monitor_index_.load();
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[idx].name.c_str());
|
||||
stream_id.c_str());
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string CleanDisplayName(const std::wstring& wide_name) {
|
||||
std::string GetDisplayLabel(const std::wstring& wide_name) {
|
||||
std::string name = WideToUtf8(wide_name);
|
||||
name.erase(std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
constexpr char kDevicePrefix[] = "\\\\.\\";
|
||||
if (name.rfind(kDevicePrefix, 0) == 0) {
|
||||
name.erase(0, sizeof(kDevicePrefix) - 1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
@@ -42,7 +44,7 @@ BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
|
||||
MONITORINFOEX mi{};
|
||||
mi.cbSize = sizeof(MONITORINFOEX);
|
||||
if (GetMonitorInfo(hMonitor, &mi)) {
|
||||
std::string name = CleanDisplayName(mi.szDevice);
|
||||
std::string name = GetDisplayLabel(mi.szDevice);
|
||||
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
|
||||
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
|
||||
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||
@@ -209,7 +211,8 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
width, height);
|
||||
|
||||
if (callback_) {
|
||||
callback_(nv12_frame_, nv12_size, width, height, di.name.c_str());
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
|
||||
}
|
||||
|
||||
SelectObject(mem_dc, old);
|
||||
@@ -221,4 +224,4 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -24,11 +25,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string CleanDisplayName(const std::wstring& wide_name) {
|
||||
std::string GetDisplayLabel(const std::wstring& wide_name) {
|
||||
std::string name = WideToUtf8(wide_name);
|
||||
name.erase(std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
constexpr char kDevicePrefix[] = "\\\\.\\";
|
||||
if (name.rfind(kDevicePrefix, 0) == 0) {
|
||||
name.erase(0, sizeof(kDevicePrefix) - 1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@ BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
|
||||
monitor_info_.cbSize = sizeof(MONITORINFOEX);
|
||||
|
||||
if (GetMonitorInfo(hmonitor, &monitor_info_)) {
|
||||
std::string display_name = CleanDisplayName(monitor_info_.szDevice);
|
||||
std::string display_name = GetDisplayLabel(monitor_info_.szDevice);
|
||||
if (monitor_info_.dwFlags & MONITORINFOF_PRIMARY) {
|
||||
gs_display_list.insert(
|
||||
gs_display_list.begin(),
|
||||
@@ -402,8 +404,9 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
(uint8_t*)(nv12_frame_ + even_width * even_height),
|
||||
even_width, even_width, even_height);
|
||||
|
||||
const std::string stream_id = MakeDisplayStreamId(id);
|
||||
on_data_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[id].name.c_str());
|
||||
stream_id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "interactive_state.h"
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_dxgi.h"
|
||||
@@ -403,37 +404,37 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
cb_ = [this](unsigned char* data, int size, int w, int h,
|
||||
const char* display_name) {
|
||||
const char* reported_stream_id) {
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* raw_display_name = display_name ? display_name : "";
|
||||
std::string mapped_name;
|
||||
const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
|
||||
std::string mapped_stream_id;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = label_alias_.find(raw_display_name);
|
||||
if (it != label_alias_.end())
|
||||
mapped_name = it->second;
|
||||
else
|
||||
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;
|
||||
auto it = stream_id_alias_.find(raw_stream_id);
|
||||
if (it != stream_id_alias_.end()) {
|
||||
mapped_stream_id = it->second;
|
||||
} else {
|
||||
// Unknown backend labels are presentation data, not protocol IDs.
|
||||
// Resolve them through the selected logical display instead.
|
||||
mapped_stream_id.clear();
|
||||
}
|
||||
mapped_stream_id = ResolveDisplayStreamId(
|
||||
mapped_stream_id.c_str(), canonical_displays_.size(),
|
||||
monitor_index_.load(std::memory_order_relaxed));
|
||||
}
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
LOG_WARN("Windows capturer dropping frame without a registered stream "
|
||||
"id: reported='{}', size={}x{}, bytes={}",
|
||||
raw_stream_id, w, h, size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
if (post_secure_desktop_waiting_for_frame_.exchange(
|
||||
false, std::memory_order_relaxed)) {
|
||||
const ULONGLONG start_tick =
|
||||
@@ -445,10 +446,11 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
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);
|
||||
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
|
||||
"elapsed_ms={}",
|
||||
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
|
||||
}
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
|
||||
};
|
||||
|
||||
int ret = -1;
|
||||
@@ -508,9 +510,8 @@ int ScreenCapturerWin::Destroy() {
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
label_alias_.clear();
|
||||
handle_to_canonical_.clear();
|
||||
canonical_labels_.clear();
|
||||
stream_id_alias_.clear();
|
||||
handle_to_canonical_index_.clear();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -523,10 +524,26 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
|
||||
show_cursor_.store(show_cursor, std::memory_order_relaxed);
|
||||
paused_.store(false, std::memory_order_relaxed);
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
|
||||
int ret = impl_->Start(show_cursor);
|
||||
// Refresh physical monitor identities before every session. HMONITOR and
|
||||
// DXGI output handles may change after an HDMI hotplug while CrossDesk stays
|
||||
// open; logical stream IDs must remain stable.
|
||||
const int requested_monitor =
|
||||
monitor_index_.load(std::memory_order_relaxed);
|
||||
impl_->Destroy();
|
||||
int ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
RebuildAliasesFromImpl();
|
||||
ret = impl_->Start(show_cursor);
|
||||
if (ret == 0 && requested_monitor > 0 &&
|
||||
impl_->SwitchTo(requested_monitor) != 0) {
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret);
|
||||
LOG_WARN("Windows capturer: refresh/start failed (ret={}), trying fallback",
|
||||
ret);
|
||||
|
||||
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
|
||||
int r = cand->Init(fps_, cb_);
|
||||
@@ -536,6 +553,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
impl_ = std::move(cand);
|
||||
impl_is_wgc_plugin_ = false;
|
||||
RebuildAliasesFromImpl();
|
||||
if (requested_monitor > 0 &&
|
||||
impl_->SwitchTo(requested_monitor) != 0) {
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -624,25 +645,49 @@ int ScreenCapturerWin::ResetToInitialMonitor() {
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
|
||||
if (!impl_) return {};
|
||||
return impl_->GetDisplayInfoList();
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
return canonical_displays_;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::BuildCanonicalFromImpl() {
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
handle_to_canonical_.clear();
|
||||
label_alias_.clear();
|
||||
handle_to_canonical_index_.clear();
|
||||
stream_id_alias_.clear();
|
||||
canonical_displays_ = impl_->GetDisplayInfoList();
|
||||
canonical_labels_.clear();
|
||||
for (const auto& di : canonical_displays_) {
|
||||
handle_to_canonical_[di.handle] = di.name;
|
||||
canonical_labels_.insert(di.name);
|
||||
std::unordered_map<std::string, size_t> name_counts;
|
||||
for (const auto& display : canonical_displays_) {
|
||||
if (!display.name.empty()) {
|
||||
++name_counts[display.name];
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
|
||||
auto& di = canonical_displays_[i];
|
||||
const std::string stream_id = MakeDisplayStreamId(i);
|
||||
if (di.name.empty()) {
|
||||
di.name = stream_id;
|
||||
}
|
||||
handle_to_canonical_index_[di.handle] = i;
|
||||
stream_id_alias_[stream_id] = stream_id;
|
||||
if (name_counts[di.name] == 1) {
|
||||
// Compatibility with an older WGC plugin that reports display names.
|
||||
stream_id_alias_[di.name] = stream_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::RebuildAliasesFromImpl() {
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
label_alias_.clear();
|
||||
stream_id_alias_.clear();
|
||||
auto current = impl_->GetDisplayInfoList();
|
||||
if (current.empty() || canonical_displays_.empty()) return;
|
||||
const auto previous_handles = handle_to_canonical_index_;
|
||||
handle_to_canonical_index_.clear();
|
||||
std::unordered_map<std::string, size_t> name_counts;
|
||||
for (const auto& display : current) {
|
||||
if (!display.name.empty()) {
|
||||
++name_counts[display.name];
|
||||
}
|
||||
}
|
||||
auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
|
||||
int dl = std::abs(a.left - b.left);
|
||||
int dt = std::abs(a.top - b.top);
|
||||
@@ -650,21 +695,52 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
|
||||
int dh = std::abs(a.height - b.height);
|
||||
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
|
||||
};
|
||||
for (const auto& di : current) {
|
||||
std::string canonical;
|
||||
auto it = handle_to_canonical_.find(di.handle);
|
||||
if (it != handle_to_canonical_.end()) {
|
||||
canonical = it->second;
|
||||
} else {
|
||||
for (const auto& c : canonical_displays_) {
|
||||
if (similar(di, c) || (di.is_primary && c.is_primary)) {
|
||||
canonical = c.name;
|
||||
std::vector<bool> used(canonical_displays_.size(), false);
|
||||
for (size_t current_index = 0; current_index < current.size();
|
||||
++current_index) {
|
||||
const auto& di = current[current_index];
|
||||
int canonical_index = -1;
|
||||
auto old_handle = previous_handles.find(di.handle);
|
||||
if (old_handle != previous_handles.end() &&
|
||||
old_handle->second < canonical_displays_.size() &&
|
||||
!used[old_handle->second]) {
|
||||
canonical_index = static_cast<int>(old_handle->second);
|
||||
}
|
||||
if (canonical_index < 0) {
|
||||
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
|
||||
if (!used[i] && (similar(di, canonical_displays_[i]) ||
|
||||
(di.is_primary && canonical_displays_[i].is_primary))) {
|
||||
canonical_index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canonical.empty() && canonical != di.name) {
|
||||
label_alias_[di.name] = canonical;
|
||||
if (canonical_index < 0 && current_index < canonical_displays_.size() &&
|
||||
!used[current_index]) {
|
||||
canonical_index = static_cast<int>(current_index);
|
||||
}
|
||||
if (canonical_index < 0) {
|
||||
LOG_WARN("Windows capturer ignoring unregistered display after topology "
|
||||
"change: display='{}', index={}",
|
||||
di.name, current_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
used[canonical_index] = true;
|
||||
auto& canonical = canonical_displays_[canonical_index];
|
||||
const std::string backend_stream_id =
|
||||
MakeDisplayStreamId(current_index);
|
||||
const std::string stable_stream_id =
|
||||
MakeDisplayStreamId(static_cast<size_t>(canonical_index));
|
||||
stream_id_alias_[backend_stream_id] = stable_stream_id;
|
||||
if (!di.name.empty() && name_counts[di.name] == 1) {
|
||||
stream_id_alias_[di.name] = stable_stream_id;
|
||||
}
|
||||
handle_to_canonical_index_[di.handle] =
|
||||
static_cast<size_t>(canonical_index);
|
||||
canonical = di;
|
||||
if (canonical.name.empty()) {
|
||||
canonical.name = stable_stream_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -792,7 +868,7 @@ bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
||||
*top = display.top;
|
||||
*width = capture_width;
|
||||
*height = capture_height;
|
||||
*display_name = display.name;
|
||||
*display_name = MakeDisplayStreamId(static_cast<size_t>(current_monitor));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
@@ -48,11 +48,11 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
cb_desktop_data cb_;
|
||||
cb_desktop_data cb_orig_;
|
||||
|
||||
std::unordered_map<void*, std::string> handle_to_canonical_;
|
||||
std::unordered_map<std::string, std::string> label_alias_;
|
||||
std::unordered_map<void*, size_t> handle_to_canonical_index_;
|
||||
std::unordered_map<std::string, std::string> stream_id_alias_;
|
||||
std::mutex alias_mutex_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
std::unordered_set<std::string> canonical_labels_;
|
||||
std::atomic<bool> invalid_stream_id_logged_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: 50ca51ecc1...7ea6e0e655
+3
-1
@@ -80,7 +80,9 @@ function setup_platform_settings()
|
||||
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
elseif is_os("macosx") then
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
if is_arch("x86_64") then
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
end
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||
|
||||
Reference in New Issue
Block a user