Compare commits

...
3 Commits
29 changed files with 1233 additions and 300 deletions
+59
View File
@@ -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_
+121 -34
View File
@@ -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);
@@ -1356,28 +1366,58 @@ void GuiApplication::BindMainCallbacks() {
if (password_change_pending_) {
return true;
}
request_id = std::to_string(++next_password_change_request_id_);
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;
{
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;
});
@@ -1788,6 +1828,7 @@ void GuiApplication::Tick() {
return;
}
HandlePasswordChangeResult();
HandleCredentialRecovery();
if (!peer_) {
CreateConnectionPeer();
}
@@ -1864,7 +1905,7 @@ void GuiApplication::Tick() {
void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false;
std::string new_password;
bool uncertain = false;
std::string error;
{
@@ -1874,6 +1915,7 @@ void GuiApplication::HandlePasswordChangeResult() {
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";
}
@@ -1882,47 +1924,43 @@ void GuiApplication::HandlePasswordChangeResult() {
}
succeeded = password_change_succeeded_;
new_password = pending_local_password_;
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) {
LOG_WARN("Password change failed: {}", error);
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;
}
std::memset(password_saved_, 0, sizeof(password_saved_));
std::strncpy(password_saved_, new_password.c_str(),
sizeof(password_saved_) - 1);
const std::string identity = std::string(client_id_) + "@" + new_password;
if (config_center_->IsSelfHosted()) {
std::memset(self_hosted_id_, 0, sizeof(self_hosted_id_));
std::strncpy(self_hosted_id_, identity.c_str(),
sizeof(self_hosted_id_) - 1);
} else {
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 (settings_.Save() != 0) {
LOG_ERROR("Password changed on server but could not be saved locally");
offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true;
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_);
@@ -1932,6 +1970,53 @@ void GuiApplication::HandlePasswordChangeResult() {
}
}
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_);
@@ -2529,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_);
+1
View File
@@ -32,6 +32,7 @@ private:
void BindServerCallbacks();
void Tick();
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
void SyncConnectionDialog();
void SyncPlatformDialogs();
+21 -6
View File
@@ -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,
@@ -80,7 +85,21 @@ int SessionDeviceManager::InitializeScreenCapturer() {
return;
}
const std::string stream_id = display_name ? display_name : "";
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() &&
@@ -113,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
+300 -36
View File
@@ -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,8 +282,9 @@ 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();
@@ -186,7 +341,8 @@ 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_));
@@ -227,27 +383,135 @@ bool SettingsManager::ActivateCachedPublicIdentity() {
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>
@@ -39,6 +40,22 @@ public:
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];
@@ -67,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_;
};
+5 -2
View File
@@ -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);
+39 -2
View File
@@ -10,6 +10,7 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include "localization.h"
#include "rd_log.h"
@@ -61,6 +62,40 @@ int GuiRuntime::CreateConnectionPeer() {
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);
@@ -147,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());
+14
View File
@@ -116,6 +116,7 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
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;
}
}
@@ -137,8 +138,21 @@ 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);
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) {
+7
View File
@@ -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;
@@ -158,10 +159,16 @@ struct PasswordChangeState {
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 {
+134 -15
View File
@@ -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 {
+4 -1
View File
@@ -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,62 @@ 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() {
const 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;
}
const CGDirectDisplayID target_display = display_it->second;
bool should_reconfigure = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (current_display_ == target_display) return 0;
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;
should_reconfigure = stream_ != nil;
current_stream_id_ = MakeDisplayStreamId(target);
should_reconfigure = capture_requested_ && stream_ != nil;
}
// Resetting session state must not create a capture stream. Preserve the
@@ -369,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_;
@@ -383,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];
@@ -394,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;
@@ -407,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) {
@@ -447,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");
@@ -523,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;
}
@@ -558,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);
}
+3 -1
View File
@@ -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};