Compare commits

...
22 Commits
Author SHA1 Message Date
dijunkun df72204619 [fix] update minirtc OpenH264 bitrate handling 2026-08-24 10:33:56 +08:00
dijunkun 3678271577 [feat] report client version and platform 2026-08-24 09:46:51 +08:00
dijunkun 6b5535ab3d [fix] keep display stream IDs stable after monitor changes 2026-08-24 00:32:43 +08:00
dijunkun 078bd1e520 [fix] recover interrupted password changes 2026-08-23 21:59:20 +08:00
dijunkun 718705c352 [fix] prevent NVCodec unload crashes, fixes #95 2026-08-23 21:16:41 +08:00
dijunkun 1ca5852280 [fix] update minirtc reliable transmission 2026-08-23 19:04:43 +08:00
dijunkun 1eda6cc183 [fix] update minirtc feedback handling 2026-08-21 16:19:49 +08:00
dijunkun 8d37f8f5fb [fix] update minirtc CBR encoder configuration 2026-08-21 15:51:20 +08:00
dijunkun 05b8aa629e [fix] update minirtc for Windows build 2026-08-21 14:36:21 +08:00
dijunkun 2cbceba953 [fix] use screen content defaults for desktop streaming 2026-08-21 14:29:44 +08:00
dijunkun 89d3c8b53f [fix] prevent expired probe cluster reuse 2026-08-20 18:30:04 +08:00
dijunkun 51a4190e97 [fix] propagate encoder bitrate update failures 2026-08-20 18:20:02 +08:00
dijunkun 3737cd0dd5 [fix] align encoder and network bitrate limits 2026-08-20 18:05:45 +08:00
dijunkun 00957d6740 [fix] serialize pacer tasks and harden probing 2026-08-20 17:42:50 +08:00
dijunkun 6ec86322b2 [fix] abort probing when ICE becomes unavailable 2026-08-20 17:12:45 +08:00
dijunkun ecc131b5ee [fix] gate media-less probing by transport capability 2026-08-20 16:56:49 +08:00
dijunkun 1aa50e9ffa [fix] schedule pacer with high precision tasks 2026-08-20 16:22:01 +08:00
dijunkun d49f5bc520 [fix] handle task queue wakeups correctly 2026-08-20 14:45:18 +08:00
dijunkun 8818b9b8be [fix] ensure video bitrate updates reach encoders 2026-08-19 16:33:47 +08:00
dijunkun 3be283238c [fix] prevent screen capture without connected peers 2026-08-19 15:51:37 +08:00
dijunkun 5d0453fff3 [fix] prevent ICE callback self-join during teardown 2026-08-18 17:10:03 +08:00
dijunkun 8a9775a571 [fix] adapt VideoToolbox encoder resolution 2026-08-18 16:42:42 +08:00
31 changed files with 1308 additions and 307 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_
+7 -5
View File
@@ -29,7 +29,11 @@ int ConfigCenter::Load() {
return -1; 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 = const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_)); 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_ = ini_.GetBoolValue(section_, "hardware_video_codec",
hardware_video_codec_); hardware_video_codec_);
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr); const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) { if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
const long parsed_turn_mode = ini_.GetLongValue( 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_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
: TURN_MODE::DISABLED; : TURN_MODE::DISABLED;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_)); 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_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
enable_self_hosted_ = enable_self_hosted_ =
@@ -121,8 +124,7 @@ int ConfigCenter::Load() {
file_transfer_save_path_ = ""; file_transfer_save_path_ = "";
} }
if (persist_turn_mode_migration && if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
ini_.SaveFile(config_path_.c_str()) < 0) {
return -1; return -1;
} }
+113 -26
View File
@@ -17,6 +17,7 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <random>
#include <shared_mutex> #include <shared_mutex>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -51,6 +52,15 @@ namespace {
using namespace std::chrono_literals; 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__) #if defined(__linux__) && !defined(__APPLE__)
bool HasNonEmptyEnvironmentVariable(const char* name) { bool HasNonEmptyEnvironmentVariable(const char* name) {
const char* value = std::getenv(name); const char* value = std::getenv(name);
@@ -1356,21 +1366,47 @@ void GuiApplication::BindMainCallbacks() {
if (password_change_pending_) { if (password_change_pending_) {
return true; 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_pending_ = true;
password_change_result_ready_ = false; password_change_result_ready_ = false;
password_change_succeeded_ = false; password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
password_change_requested_at_ = std::chrono::steady_clock::now(); password_change_requested_at_ = std::chrono::steady_clock::now();
pending_password_change_request_id_ = request_id; pending_password_change_request_id_ = request_id;
pending_local_password_ = password; pending_local_password_ = password;
password_change_error_.clear(); 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"}, const nlohmann::json request = {{"type", "change_password"},
{"request_id", request_id}, {"request_id", request_id},
{"new_password", password}}; {"new_password", password}};
const std::string message = request.dump(); const std::string message = request.dump();
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) { if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
{
std::lock_guard<std::mutex> lock(password_change_mutex_); std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false; password_change_pending_ = false;
pending_password_change_request_id_.clear(); pending_password_change_request_id_.clear();
@@ -1379,6 +1415,10 @@ void GuiApplication::BindMainCallbacks() {
localization::signal_disconnected[localization_language_index_]; localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
} }
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Could not clear unsent pending password change");
}
}
return true; return true;
}); });
main->on_connect_requested( main->on_connect_requested(
@@ -1788,6 +1828,7 @@ void GuiApplication::Tick() {
return; return;
} }
HandlePasswordChangeResult(); HandlePasswordChangeResult();
HandleCredentialRecovery();
if (!peer_) { if (!peer_) {
CreateConnectionPeer(); CreateConnectionPeer();
} }
@@ -1864,7 +1905,7 @@ void GuiApplication::Tick() {
void GuiApplication::HandlePasswordChangeResult() { void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false; bool succeeded = false;
std::string new_password; bool uncertain = false;
std::string error; std::string error;
{ {
@@ -1874,6 +1915,7 @@ void GuiApplication::HandlePasswordChangeResult() {
std::chrono::seconds(10)) { std::chrono::seconds(10)) {
password_change_result_ready_ = true; password_change_result_ready_ = true;
password_change_succeeded_ = false; password_change_succeeded_ = false;
password_change_result_uncertain_ = true;
password_change_error_ = "Server did not respond"; password_change_error_ = "Server did not respond";
} }
@@ -1882,47 +1924,43 @@ void GuiApplication::HandlePasswordChangeResult() {
} }
succeeded = password_change_succeeded_; succeeded = password_change_succeeded_;
new_password = pending_local_password_; uncertain = password_change_result_uncertain_;
error = password_change_error_; error = password_change_error_;
password_change_pending_ = false; password_change_pending_ = false;
password_change_result_ready_ = false; password_change_result_ready_ = false;
password_change_succeeded_ = false; password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
pending_password_change_request_id_.clear(); pending_password_change_request_id_.clear();
pending_local_password_.clear(); pending_local_password_.clear();
password_change_error_.clear(); password_change_error_.clear();
} }
if (!succeeded) { if (!succeeded) {
if (uncertain) {
LOG_WARN("Password change outcome is unknown: {}; re-authenticating",
error);
} else {
LOG_WARN("Password change failed: {}", error); 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_]; offline_warning_text_ = localization::failed[localization_language_index_];
if (!error.empty()) { if (!error.empty()) {
offline_warning_text_ += ": " + error; offline_warning_text_ += ": " + error;
} }
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
if (uncertain && peer_) {
LeaveConnection(peer_, client_id_);
DestroyPeer(&peer_);
}
return; return;
} }
std::memset(password_saved_, 0, sizeof(password_saved_)); if (!settings_.PromotePendingPasswordChange()) {
std::strncpy(password_saved_, new_password.c_str(), // The pending credential was persisted before the request was sent, so it
sizeof(password_saved_) - 1); // remains sufficient for recovery even when promotion cannot be written.
LOG_WARN("Password changed on server; pending recovery record retained");
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;
} }
LOG_INFO("Password changed successfully for [{}]", client_id_); 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() { void GuiApplication::UpdateLocalization() {
const int language = const int language =
localization::detail::ClampLanguageIndex(localization_language_index_); localization::detail::ClampLanguageIndex(localization_language_index_);
@@ -2529,8 +2614,10 @@ void GuiApplication::SyncStreamWindow() {
std::vector<slint::SharedString> displays; std::vector<slint::SharedString> displays;
displays.reserve(props->display_info_list_.size()); displays.reserve(props->display_info_list_.size());
for (const auto& display : props->display_info_list_) { for (size_t index = 0; index < props->display_info_list_.size(); ++index) {
displays.emplace_back(UiText(display.name)); 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_->display_model->set_vector(std::move(displays));
(*ui_->stream)->set_selected_display(props->selected_display_); (*ui_->stream)->set_selected_display(props->selected_display_);
+1
View File
@@ -32,6 +32,7 @@ private:
void BindServerCallbacks(); void BindServerCallbacks();
void Tick(); void Tick();
void HandlePasswordChangeResult(); void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow(); void SyncMainWindow();
void SyncConnectionDialog(); void SyncConnectionDialog();
void SyncPlatformDialogs(); void SyncPlatformDialogs();
@@ -6,6 +6,7 @@
#ifndef _LOCALIZATION_H_ #ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_ #define _LOCALIZATION_H_
#include <cstddef>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
@@ -115,6 +116,20 @@ inline const std::string& LocalizedString::operator[](
return detail::GetTranslatedText(key_, language_index); 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) \ #define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name); inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING) CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
@@ -35,6 +35,7 @@ struct TranslationRow {
u8"Недавние подключения") \ u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \ X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(display_screen, u8"显示屏", "Display", u8"Экран") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \ X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \ u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
@@ -3,6 +3,7 @@
#include <chrono> #include <chrono>
#include <cstdlib> #include <cstdlib>
#include "display_stream_id.h"
#include "platform.h" #include "platform.h"
#include "rd_log.h" #include "rd_log.h"
#include "runtime/gui_runtime.h" #include "runtime/gui_runtime.h"
@@ -47,6 +48,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
? 30 ? 30
: 60; : 60;
LOG_INFO("Init screen capturer with {} fps", fps); 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( const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height, fps, [this, fps](unsigned char *data, int size, int width, int height,
@@ -60,7 +65,41 @@ int SessionDeviceManager::InitializeScreenCapturer() {
return; 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 = const bool resumed_after_gap =
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs; last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed = !last_video_frame_stream_id_.empty() && const bool stream_changed = !last_video_frame_stream_id_.empty() &&
@@ -79,7 +118,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
frame.width = width; frame.width = width;
frame.height = height; frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_); 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_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time; last_frame_time_ = now_time;
}); });
@@ -90,6 +132,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
if (!latest_display_info.empty()) { if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info; display_info_list_ = latest_display_info;
} }
registered_display_stream_count_ = display_info_list_.size();
return 0; return 0;
} }
@@ -77,10 +77,12 @@ private:
MouseController *mouse_controller_ = nullptr; MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr; KeyboardCapturer *keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_; std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_; std::mutex captured_keyboard_inputs_mutex_;
uint64_t last_frame_time_ = 0; uint64_t last_frame_time_ = 0;
std::string last_video_frame_stream_id_; std::string last_video_frame_stream_id_;
bool invalid_video_stream_id_logged_ = false;
}; };
} // namespace crossdesk } // namespace crossdesk
+296 -32
View File
@@ -1,11 +1,24 @@
#include "features/settings/settings_manager.h" #include "features/settings/settings_manager.h"
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstring> #include <cstring>
#include <exception> #include <exception>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <functional>
#include <memory> #include <memory>
#include <string> #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 "localization.h"
#include "rd_log.h" #include "rd_log.h"
@@ -14,6 +27,9 @@
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
constexpr uint32_t kCacheV3Version = 3;
template <size_t Size> template <size_t Size>
void CopyString(char (&destination)[Size], const char *source) { void CopyString(char (&destination)[Size], const char *source) {
static_assert(Size > 0); 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 } // namespace
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {} SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
@@ -33,36 +142,74 @@ int SettingsManager::Save() {
} }
int SettingsManager::SaveLocked() { 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, CopyString(cache_v2_.client_id_with_password,
owner_.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_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_); CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_), cache_v3_.magic = kCacheV3Magic;
sizeof(cache_v2_)); 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. // 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, CopyString(cache_v1_.client_id_with_password,
owner_.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_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_), if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
sizeof(cache_v1_)); &cache_v1_, sizeof(cache_v1_))) {
LOG_WARN("Failed to update legacy v1 credential cache");
} }
return 0; 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() { bool SettingsManager::ReadV2Locked() {
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
std::ios::binary); std::ios::binary);
@@ -84,13 +231,20 @@ bool SettingsManager::ReadV2Locked() {
int SettingsManager::Load() { int SettingsManager::Load() {
std::unique_lock<std::mutex> lock(cache_mutex_); 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_, CopyString(owner_.client_id_with_password_,
cache_v2_.client_id_with_password); cache_v2_.client_id_with_password);
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id); 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_key_, cache_v2_.key, sizeof(cache_v2_.key));
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv)); 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 { } else {
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
std::ios::binary); 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_key_, cache_v1_.key, sizeof(cache_v1_.key));
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv)); std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
cache_v3_ = {};
SaveLocked(); SaveLocked();
LOG_INFO("Migrated settings from v1 to v2 cache file"); LOG_INFO("Migrated settings from v1 to v3 cache file");
} }
lock.unlock(); lock.unlock();
@@ -186,7 +341,8 @@ int SettingsManager::Load() {
bool SettingsManager::LoadCachedSelfHostedIdentity() { bool SettingsManager::LoadCachedSelfHostedIdentity() {
std::lock_guard<std::mutex> lock(cache_mutex_); 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_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_)); std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
@@ -227,27 +383,135 @@ bool SettingsManager::ActivateCachedPublicIdentity() {
return owner_.client_id_[0] != '\0'; 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) { void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
if (!client_id) { if (!client_id) {
return; return;
} }
std::lock_guard<std::mutex> lock(cache_mutex_); std::lock_guard<std::mutex> lock(cache_mutex_);
if (!ReadV2Locked()) { CopyString(owner_.self_hosted_id_, client_id);
cache_v2_ = {}; if (SaveLocked() != 0) {
} LOG_ERROR("Failed to persist self-hosted identity atomically");
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_));
} }
} }
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_ #ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_ #define CROSSDESK_GUI_SETTINGS_MANAGER_H_
#include <cstdint>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -39,6 +40,22 @@ public:
bool ActivateCachedPublicIdentity(); bool ActivateCachedPublicIdentity();
void PersistSelfHostedIdentity(const char *client_id); 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: private:
struct CacheV1 { struct CacheV1 {
char client_id_with_password[17]; char client_id_with_password[17];
@@ -67,12 +84,27 @@ private:
char self_hosted_id[17]; 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(); int SaveLocked();
bool ReadV3Locked();
bool ReadV2Locked(); bool ReadV2Locked();
void ActivateIdentity(const char *identity, bool self_hosted);
GuiRuntime &owner_; GuiRuntime &owner_;
CacheV1 cache_v1_{}; CacheV1 cache_v1_{};
CacheV2 cache_v2_{}; CacheV2 cache_v2_{};
CacheV3 cache_v3_{};
mutable std::mutex cache_mutex_; mutable std::mutex cache_mutex_;
std::unordered_map<std::string, std::string> recent_connection_aliases_; std::unordered_map<std::string, std::string> recent_connection_aliases_;
}; };
+5 -2
View File
@@ -8,6 +8,7 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "localization.h" #include "localization.h"
#include "platform.h" #include "platform.h"
#include "rd_log.h" #include "rd_log.h"
@@ -381,8 +382,10 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
return -1; return -1;
} }
for (const auto& display_info : devices_.display_info_list()) { const auto& displays = devices_.display_info_list();
AddVideoStream(props->peer_, display_info.name.c_str()); 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()); AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false); AddDataStream(props->peer_, props->data_label_.c_str(), false);
+45 -2
View File
@@ -10,6 +10,7 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
@@ -61,6 +62,40 @@ int GuiRuntime::CreateConnectionPeer() {
params_.user_id = client_id_with_password_; 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 // self hosted server config
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(), strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1); sizeof(signal_server_ip_self_) - 1);
@@ -111,8 +146,14 @@ int GuiRuntime::CreateConnectionPeer() {
: false; : false;
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode()); params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
params_.enable_srtp = config_center_->IsEnableSrtp(); params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_content_type = VideoContentType::ScreenContent;
params_.video_quality = params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality()); 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_video_buffer = nullptr;
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer; params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer; params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
@@ -141,8 +182,10 @@ int GuiRuntime::CreateConnectionPeer() {
} }
if (0 == devices_.InitializeScreenCapturer()) { if (0 == devices_.InitializeScreenCapturer()) {
for (const auto &display_info : devices_.display_info_list()) { const auto &displays = devices_.display_info_list();
AddVideoStream(peer_, display_info.name.c_str()); 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()); AddAudioStream(peer_, audio_label_.c_str());
+41
View File
@@ -26,6 +26,31 @@
namespace crossdesk { 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) {} PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char* message, size_t size, void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
@@ -116,6 +141,7 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
j.contains("reason") && j["reason"].is_string() j.contains("reason") && j["reason"].is_string()
? j["reason"].get<std::string>() ? j["reason"].get<std::string>()
: "Password change failed"; : "Password change failed";
runtime->password_change_result_uncertain_ = false;
runtime->password_change_result_ready_ = true; runtime->password_change_result_ready_ = true;
} }
} }
@@ -137,8 +163,22 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
runtime->signal_connected_ = true; runtime->signal_connected_ = true;
runtime->need_to_send_recent_connections_ = true; runtime->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id); 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) { } else if (SignalStatus::SignalFailed == status) {
runtime->signal_connected_ = false; 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) { } else if (SignalStatus::SignalClosed == status) {
runtime->signal_connected_ = false; runtime->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) { } else if (SignalStatus::SignalReconnecting == status) {
@@ -166,6 +206,7 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
} else if (SignalStatus::SignalConnected == status) { } else if (SignalStatus::SignalConnected == status) {
props->signal_connected_ = true; props->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", remote_id); LOG_INFO("[{}] connected to signal server", remote_id);
SendClientInfo(props->peer_, client_id);
} else if (SignalStatus::SignalFailed == status) { } else if (SignalStatus::SignalFailed == status) {
props->signal_connected_ = false; props->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) { } else if (SignalStatus::SignalClosed == status) {
+3
View File
@@ -2,6 +2,7 @@
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_ #define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#include <cstddef> #include <cstddef>
#include <string>
#include "minirtc.h" #include "minirtc.h"
@@ -39,6 +40,8 @@ public:
void *user_data); void *user_data);
private: private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_; GuiRuntime &owner_;
}; };
+7
View File
@@ -116,6 +116,7 @@ struct UserSettingsState {
char password_saved_[7] = ""; char password_saved_[7] = "";
char self_hosted_id_[17] = ""; char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = ""; char self_hosted_user_id_[17] = "";
char connection_login_identity_[17] = "";
int language_button_value_ = 0; int language_button_value_ = 0;
int video_quality_button_value_ = 2; int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1; int video_frame_rate_button_value_ = 1;
@@ -158,10 +159,16 @@ struct PasswordChangeState {
bool password_change_pending_ = false; bool password_change_pending_ = false;
bool password_change_result_ready_ = false; bool password_change_result_ready_ = false;
bool password_change_succeeded_ = false; bool password_change_succeeded_ = false;
bool password_change_result_uncertain_ = false;
std::chrono::steady_clock::time_point password_change_requested_at_; std::chrono::steady_clock::time_point password_change_requested_at_;
std::string pending_password_change_request_id_; std::string pending_password_change_request_id_;
std::string pending_local_password_; std::string pending_local_password_;
std::string password_change_error_; 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 { struct ConnectionState {
+134 -15
View File
@@ -167,6 +167,8 @@ export component StreamWindow inherits Window {
private property <bool> control-expanded: true; private property <bool> control-expanded: true;
private property <bool> display-menu-open: false; 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> shortcut-menu-open: false;
private property <bool> control-docked-left: true; private property <bool> control-docked-left: true;
private property <bool> control-dragging: false; private property <bool> control-dragging: false;
@@ -183,6 +185,48 @@ export component StreamWindow inherits Window {
callback begin-control-drag(length, length); callback begin-control-drag(length, length);
callback move-control-drag(length, length); callback move-control-drag(length, length);
callback end-control-drag; 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) => { begin-control-drag(press-x, press-y) => {
// Capture the snapped position before switching x to control-drag-x. // 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. // pointer positions remain relative to the stationary video surface.
root.control-press-x = press-x - current-x; root.control-press-x = press-x - current-x;
root.control-press-y = press-y - current-y; 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.display-menu-open = false;
root.shortcut-menu-open = false; root.shortcut-menu-open = false;
root.control-dragging = true; root.control-dragging = true;
@@ -503,7 +550,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width && self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y && self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height; && 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(); input-focus.focus();
} }
if event.kind == PointerEventKind.down if event.kind == PointerEventKind.down
@@ -515,9 +564,9 @@ export component StreamWindow inherits Window {
|| event.kind == PointerEventKind.cancel) { || event.kind == PointerEventKind.cancel) {
root.end-control-drag(); root.end-control-drag();
// The control bar overlays the remote video. Events in // 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. // 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); 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) => { scroll-event(event) => {
if self.mouse-x < control.x if !root.is-local-control-area(self.mouse-x, self.mouse-y) {
|| self.mouse-x > control.x + control.width
|| self.mouse-y < control.y
|| self.mouse-y > control.y + control.height {
root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y); root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y);
} }
accept accept
@@ -565,12 +611,27 @@ export component StreamWindow inherits Window {
icon: FontAwesomeIcons.display; icon: FontAwesomeIcons.display;
badge: root.selected-display + 1; badge: root.selected-display + 1;
tooltip: StreamStrings.select-display; 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 { if root.control-expanded: shortcut-button := ControlBarButton {
x: root.control-docked-left ? 41px : 74px; y: 7px; x: root.control-docked-left ? 41px : 74px; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut; 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 { if root.control-expanded: mouse-button := ControlBarButton {
x: root.control-docked-left ? 73px : 106px; y: 7px; x: root.control-docked-left ? 73px : 106px; y: 7px;
@@ -753,17 +814,75 @@ export component StreamWindow inherits Window {
} }
} }
if root.display-menu-open: Rectangle { if root.display-menu-open: display-menu := Rectangle {
x: control.x + (root.control-docked-left ? 9px : 42px); y: control.y + 40px; width: 180px; height: min(180px, root.displays.length * 30px + 8px); private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11; 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 { VerticalLayout {
padding: 4px; spacing: 1px; padding: 4px; spacing: 1px;
for display[index] in root.displays: Rectangle { for display[index] in root.displays: Rectangle {
height: 29px; height: 29px;
background: display-touch.has-hover || index == root.selected-display ? #e8eef9 : transparent; background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2
Text { x: 8px; text: display; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; } : index == display-menu.hovered-index ? #d8e6f8
display-touch := TouchArea { clicked => { root.selected-display = index; root.switch-display(index); root.display-menu-open = false; } } : 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 }
} }
} }
+4 -1
View File
@@ -108,7 +108,10 @@ int GuiApplication::ControlBar(
if (ImGui::BeginPopup("display")) { if (ImGui::BeginPopup("display")) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < props->display_info_list_.size(); i++) { 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; props->selected_display_ = i;
RemoteAction remote_action; RemoteAction remote_action;
@@ -26,6 +26,7 @@
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -262,6 +263,7 @@ bool ScreenCapturerDrm::DiscoverOutputs() {
output.height = static_cast<int>(crtc->height); output.height = static_cast<int>(crtc->height);
output.name = std::string(ConnectorTypeName(connector->connector_type)) + output.name = std::string(ConnectorTypeName(connector->connector_type)) +
std::to_string(connector->connector_type_id); std::to_string(connector->connector_type_id);
output.stream_id = MakeDisplayStreamId(outputs_.size());
outputs_.push_back(output); outputs_.push_back(output);
display_info_list_.push_back( display_info_list_.push_back(
@@ -427,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
if (emit_callback && callback_) { if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width, 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); UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
@@ -47,6 +47,7 @@ class ScreenCapturerDrm : public ScreenCapturer {
uint32_t connector_id = 0; uint32_t connector_id = 0;
uint32_t crtc_id = 0; uint32_t crtc_id = 0;
std::string name; std::string name;
std::string stream_id;
int left = 0; int left = 0;
int top = 0; int top = 0;
int width = 0; int width = 0;
@@ -6,6 +6,7 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "display_stream_id.h"
#include "platform.h" #include "platform.h"
#include "rd_log.h" #include "rd_log.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM #if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
@@ -49,10 +50,21 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
fps_ = fps; fps_ = fps;
callback_orig_ = std::move(cb); callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height, callback_ = [this](unsigned char* data, int size, int width, int height,
const char* display_name) { const char* reported_stream_id) {
const std::string mapped_name = MapDisplayName(display_name); 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_) { 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; backend_ = BackendType::kNone;
callback_ = nullptr; callback_ = nullptr;
callback_orig_ = 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_); std::lock_guard<std::mutex> lock(alias_mutex_);
canonical_displays_.clear(); canonical_displays_.clear();
label_alias_.clear(); stream_id_alias_.clear();
} }
return 0; return 0;
} }
@@ -153,15 +167,12 @@ int ScreenCapturerLinux::Start(bool show_cursor) {
return -1; return -1;
} }
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER // X11 output names, DRM connectors and Wayland stream handles may all change
if (backend_ == BackendType::kWayland) { // after an HDMI hotplug, so rebuild the current backend before each session.
const int refresh_ret = RefreshWaylandBackend(); const int refresh_ret = RefreshCurrentBackend();
if (refresh_ret != 0) { if (refresh_ret != 0) {
LOG_WARN("Linux screen capturer Wayland backend refresh failed: {}", LOG_WARN("Linux screen capturer backend refresh failed: {}", refresh_ret);
refresh_ret);
} }
}
#endif
const int ret = impl_->Start(show_cursor); const int ret = impl_->Start(show_cursor);
if (ret == 0) { if (ret == 0) {
@@ -235,14 +246,23 @@ int ScreenCapturerLinux::SwitchTo(int monitor_index) {
if (!impl_) { if (!impl_) {
return -1; 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() { int ScreenCapturerLinux::ResetToInitialMonitor() {
if (!impl_) { if (!impl_) {
return -1; 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() { std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
@@ -320,27 +340,55 @@ int ScreenCapturerLinux::InitWayland() {
#endif #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 #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_); const int ret = backend->Init(fps_, callback_);
if (ret != 0) { if (ret != 0) {
backend->Destroy(); backend->Destroy();
return ret; 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_) { if (impl_) {
impl_->Destroy(); impl_->Destroy();
} }
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend); impl_ = std::move(backend);
backend_ = BackendType::kWayland; LOG_INFO("Linux screen capturer {} backend refreshed before start",
LOG_INFO("Linux screen capturer Wayland backend refreshed before start"); backend_name);
return 0; return 0;
#else
return -1;
#endif
} }
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) { bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
@@ -447,61 +495,82 @@ void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
} }
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear(); stream_id_alias_.clear();
if (canonical_displays_.empty()) { if (canonical_displays_.empty()) {
canonical_displays_ = backend_displays; canonical_displays_ = backend_displays;
for (const auto& display : backend_displays) { for (size_t i = 0; i < backend_displays.size(); ++i) {
label_alias_[display.name] = display.name; 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; return;
} }
if (canonical_displays_.size() < backend_displays.size()) { if (canonical_displays_.size() < backend_displays.size()) {
for (size_t i = canonical_displays_.size(); i < backend_displays.size(); LOG_WARN("Linux capturer detected {} additional display(s); they remain "
++i) { "unavailable until peer streams are reinitialized",
canonical_displays_.push_back(backend_displays[i]); backend_displays.size() - canonical_displays_.size());
}
} }
for (size_t i = 0; i < backend_displays.size(); ++i) { auto similar = [](const DisplayInfo& current, const DisplayInfo& canonical) {
const std::string mapped_name = i < canonical_displays_.size() return std::abs(current.left - canonical.left) <= 10 &&
? canonical_displays_[i].name std::abs(current.top - canonical.top) <= 10 &&
: backend_displays[i].name; std::abs(current.width - canonical.width) <= 20 &&
label_alias_[backend_displays[i].name] = mapped_name; 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()) { used[canonical_index] = true;
// Keep original stable names, but refresh geometry from active backend. const std::string backend_stream_id =
canonical_displays_[i].handle = backend_displays[i].handle; MakeDisplayStreamId(current_index);
canonical_displays_[i].is_primary = backend_displays[i].is_primary; const std::string stable_stream_id =
canonical_displays_[i].left = backend_displays[i].left; MakeDisplayStreamId(static_cast<size_t>(canonical_index));
canonical_displays_[i].top = backend_displays[i].top; stream_id_alias_[backend_stream_id] = stable_stream_id;
canonical_displays_[i].right = backend_displays[i].right;
canonical_displays_[i].bottom = backend_displays[i].bottom; // Refresh the user-visible label and geometry. The MiniRTC stream ID is
canonical_displays_[i].width = backend_displays[i].width; // derived independently from this stable logical index.
canonical_displays_[i].height = backend_displays[i].height; 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( std::string ScreenCapturerLinux::MapStreamId(
const char* display_name) const { const char* reported_stream_id) const {
std::string input_name = display_name ? display_name : ""; std::string input_id = reported_stream_id ? reported_stream_id : "";
if (input_name.empty()) {
return input_name;
}
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(input_name); auto it = stream_id_alias_.find(input_id);
if (it != label_alias_.end()) { if (it != stream_id_alias_.end()) {
return it->second; input_id = it->second;
} else {
input_id.clear();
} }
if (canonical_displays_.size() == 1) { return ResolveDisplayStreamId(
return canonical_displays_[0].name; input_id.c_str(), canonical_displays_.size(),
} current_monitor_index_.load(std::memory_order_relaxed));
return input_name;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -7,6 +7,7 @@
#ifndef _SCREEN_CAPTURER_LINUX_H_ #ifndef _SCREEN_CAPTURER_LINUX_H_
#define _SCREEN_CAPTURER_LINUX_H_ #define _SCREEN_CAPTURER_LINUX_H_
#include <atomic>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
@@ -43,12 +44,12 @@ class ScreenCapturerLinux : public ScreenCapturer {
int InitX11(); int InitX11();
int InitDrm(); int InitDrm();
int InitWayland(); int InitWayland();
int RefreshWaylandBackend(); int RefreshCurrentBackend();
bool TryFallbackToDrm(bool show_cursor); bool TryFallbackToDrm(bool show_cursor);
bool TryFallbackToX11(bool show_cursor); bool TryFallbackToX11(bool show_cursor);
bool TryFallbackToWayland(bool show_cursor); bool TryFallbackToWayland(bool show_cursor);
void UpdateAliasesFromBackend(ScreenCapturer* backend); void UpdateAliasesFromBackend(ScreenCapturer* backend);
std::string MapDisplayName(const char* display_name) const; std::string MapStreamId(const char* reported_stream_id) const;
private: private:
std::unique_ptr<ScreenCapturer> impl_; std::unique_ptr<ScreenCapturer> impl_;
@@ -58,7 +59,10 @@ class ScreenCapturerLinux : public ScreenCapturer {
cb_desktop_data callback_orig_; cb_desktop_data callback_orig_;
std::vector<DisplayInfo> canonical_displays_; std::vector<DisplayInfo> canonical_displays_;
mutable std::mutex alias_mutex_; 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 } // namespace crossdesk
@@ -15,6 +15,7 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -831,8 +832,9 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end()); nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) { if (callback_) {
const std::string stream_id = MakeDisplayStreamId(0);
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width, 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()); pipewire_last_frame_ms_.store(NowMs());
@@ -10,6 +10,7 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.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); 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( display_info_list_.push_back(DisplayInfo(
(void*)display_, name, true, crtc_info->x, crtc_info->y, (void*)display_, name, true, crtc_info->x, crtc_info->y,
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height)); 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()); nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) { if (callback_) {
const std::string stream_id = MakeDisplayStreamId(monitor_index);
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_, callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
display_info_list_[monitor_index].name.c_str()); stream_id.c_str());
} }
XDestroyImage(image); XDestroyImage(image);
@@ -10,21 +10,21 @@
#include "screen_capturer_sck.h" #include "screen_capturer_sck.h"
#include <AppKit/AppKit.h>
#include <ApplicationServices/ApplicationServices.h> #include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h> #include <CoreGraphics/CoreGraphics.h>
#include <IOKit/IOKitLib.h> #include <IOKit/IOKitLib.h>
#include <IOKit/graphics/IOGraphicsLib.h> #include <IOKit/graphics/IOGraphicsLib.h>
#include <IOSurface/IOSurface.h> #include <IOSurface/IOSurface.h>
#include <ScreenCaptureKit/ScreenCaptureKit.h> #include <ScreenCaptureKit/ScreenCaptureKit.h>
#include <algorithm>
#include <atomic> #include <atomic>
#include <cctype>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include "display_info.h" #include "display_info.h"
#include "display_stream_id.h"
#include "rd_log.h" #include "rd_log.h"
using namespace crossdesk; using namespace crossdesk;
@@ -83,20 +83,23 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int Resume(int monitor_index) override { return 0; } 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; int ResetToInitialMonitor() override;
private: private:
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
std::map<int, CGDirectDisplayID> display_id_map_; 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; unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0; size_t nv12_frame_size_ = 0;
int width_ = 0; int width_ = 0;
int height_ = 0; int height_ = 0;
int fps_ = 60; int fps_ = 60;
bool show_cursor_ = false; bool show_cursor_ = false;
bool capture_requested_ = false;
bool invalid_stream_id_logged_ = false;
public: public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be // 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. // support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0; CGDirectDisplayID current_display_ = 0;
int initial_monitor_index_ = 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) { 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_iterator_t iter;
io_service_t serv = 0, matched_serv = 0; io_service_t serv = 0, matched_serv = 0;
@@ -212,8 +244,6 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
display_info_list_.clear(); display_info_list_.clear();
display_id_map_.clear(); display_id_map_.clear();
display_id_map_reverse_.clear();
display_id_name_map_.clear();
if (nv12_frame_) { if (nv12_frame_) {
delete[] nv12_frame_; delete[] nv12_frame_;
@@ -232,8 +262,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
fps_ = fps > 0 ? fps : 60; fps_ = fps > 0 ? fps : 60;
display_info_list_.clear(); display_info_list_.clear();
display_id_map_.clear(); display_id_map_.clear();
display_id_map_reverse_.clear();
display_id_name_map_.clear();
if (@available(macOS 10.15, *)) { if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess(); bool has_permission = CGPreflightScreenCaptureAccess();
@@ -265,26 +293,18 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
return -1; return -1;
} }
CGDirectDisplayID displays[10];
uint32_t count;
CGGetActiveDisplayList(10, displays, &count);
int unnamed_count = 1;
for (SCDisplay *display in content.displays) { for (SCDisplay *display in content.displays) {
CGDirectDisplayID display_id = display.displayID; CGDirectDisplayID display_id = display.displayID;
CGRect bounds = CGDisplayBounds(display_id); CGRect bounds = CGDisplayBounds(display_id);
bool is_primary = CGDisplayIsMain(display_id); bool is_primary = CGDisplayIsMain(display_id);
std::string name = GetDisplayName(display_id); std::string name = GetDisplayName(display_id);
if (name.empty()) { if (name.empty()) {
name = "Display" + std::to_string(unnamed_count++); name = MakeDisplayStreamId(display_info_list_.size());
} }
// clean display name, remove non-alphanumeric characters LOG_INFO("macOS display discovered: index={}, display_id={}, name='{}'",
name.erase( display_info_list_.size(), display_id, name);
std::remove_if(name.begin(), name.end(), [](unsigned char c) { return !std::isalnum(c); }),
name.end());
DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary, DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary,
static_cast<int>(bounds.origin.x), static_cast<int>(bounds.origin.y), 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_info_list_.push_back(info);
display_id_map_[display_info_list_.size() - 1] = display_id; 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; 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; return 0;
} }
@@ -307,49 +328,69 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
return -1; return -1;
} }
{
std::lock_guard<std::mutex> lock(lock_);
if (display_info_list_.empty()) { if (display_info_list_.empty()) {
LOG_ERROR("Cannot start capturer: display info not initialized"); LOG_ERROR("Cannot start capturer: display info not initialized");
return -1; return -1;
} }
show_cursor_ = show_cursor; show_cursor_ = show_cursor;
capture_requested_ = true;
invalid_stream_id_logged_ = false;
}
StartOrReconfigureCapturer(); StartOrReconfigureCapturer();
return 0; return 0;
} }
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) { int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
bool should_reconfigure = false;
{
std::lock_guard<std::mutex> lock(lock_);
auto display_it = display_id_map_.find(monitor_index); auto display_it = display_id_map_.find(monitor_index);
if (display_it == display_id_map_.end()) { 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={}", LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
monitor_index, display_id_map_.size()); monitor_index, display_id_map_.size());
return -1; return -1;
} }
current_monitor_index_ = monitor_index;
const CGDirectDisplayID target_display = display_it->second; current_display_ = display_it->second;
{ current_stream_id_ = MakeDisplayStreamId(monitor_index);
std::lock_guard<std::mutex> lock(lock_); should_reconfigure = capture_requested_;
current_display_ = target_display;
} }
if (should_reconfigure) {
StartOrReconfigureCapturer(); StartOrReconfigureCapturer();
}
return 0; return 0;
} }
int ScreenCapturerSckImpl::ResetToInitialMonitor() { int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_; const int target = initial_monitor_index_;
bool should_reconfigure = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (display_info_list_.empty()) return -1; if (display_info_list_.empty()) return -1;
auto display_it = display_id_map_.find(target); auto display_it = display_id_map_.find(target);
if (display_it == display_id_map_.end()) { if (display_it == display_id_map_.end()) {
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target); LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
return -1; return -1;
} }
const CGDirectDisplayID target_display = display_it->second;
CGDirectDisplayID target_display = display_it->second; if (current_display_ == target_display &&
if (current_display_ == target_display) return 0; current_monitor_index_ == target) {
{ return 0;
std::lock_guard<std::mutex> lock(lock_);
current_display_ = target_display;
} }
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; return 0;
} }
@@ -362,7 +403,9 @@ int ScreenCapturerSckImpl::Destroy() {
[stream_ stopCaptureWithCompletionHandler:nil]; [stream_ stopCaptureWithCompletionHandler:nil];
stream_ = nil; stream_ = nil;
} }
capture_requested_ = false;
current_display_ = 0; current_display_ = 0;
current_stream_id_.clear();
permanent_error_ = false; permanent_error_ = false;
_on_data = nullptr; _on_data = nullptr;
helper_to_release = helper_; helper_to_release = helper_;
@@ -376,6 +419,7 @@ int ScreenCapturerSckImpl::Destroy() {
int ScreenCapturerSckImpl::Stop() { int ScreenCapturerSckImpl::Stop() {
std::lock_guard<std::mutex> lock(lock_); std::lock_guard<std::mutex> lock(lock_);
capture_requested_ = false;
if (stream_) { if (stream_) {
LOG_INFO("Stopping stream"); LOG_INFO("Stopping stream");
[stream_ stopCaptureWithCompletionHandler:nil]; [stream_ stopCaptureWithCompletionHandler:nil];
@@ -387,6 +431,14 @@ int ScreenCapturerSckImpl::Stop() {
} }
void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *content) { 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) { if (!content) {
LOG_ERROR("getShareableContent failed"); LOG_ERROR("getShareableContent failed");
permanent_error_ = true; permanent_error_ = true;
@@ -400,22 +452,71 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
} }
SCDisplay *captured_display = nil; SCDisplay *captured_display = nil;
bool show_cursor = false;
{ {
std::lock_guard<std::mutex> lock(lock_); 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) { for (SCDisplay *display in content.displays) {
if (display_index == logical_index) logical_fallback = display;
if (current_display_ != 0 && current_display_ == display.displayID) { if (current_display_ != 0 && current_display_ == display.displayID) {
LOG_WARN("current display: {}, name: {}", current_display_,
display_id_name_map_[current_display_]);
captured_display = display; captured_display = display;
break; break;
} }
++display_index;
} }
if (!captured_display) { if (!captured_display) {
captured_display = content.displays.firstObject; captured_display = logical_fallback ? logical_fallback
: content.displays.firstObject;
if (!logical_fallback) logical_index = 0;
}
if (captured_display) { if (captured_display) {
current_display_ = captured_display.displayID; 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) { if (!captured_display) {
@@ -440,13 +541,14 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
} }
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
config.showsCursor = show_cursor_; config.showsCursor = show_cursor;
config.width = filter.contentRect.size.width * filter.pointPixelScale; config.width = filter.contentRect.size.width * filter.pointPixelScale;
config.height = filter.contentRect.size.height * filter.pointPixelScale; config.height = filter.contentRect.size.height * filter.pointPixelScale;
config.captureResolution = SCCaptureResolutionAutomatic; config.captureResolution = SCCaptureResolutionAutomatic;
config.minimumFrameInterval = CMTimeMake(1, fps_); config.minimumFrameInterval = CMTimeMake(1, fps_);
std::lock_guard<std::mutex> lock(lock_); std::lock_guard<std::mutex> lock(lock_);
if (!capture_requested_) return;
if (stream_) { if (stream_) {
LOG_INFO("Updating stream configuration"); LOG_INFO("Updating stream configuration");
@@ -516,7 +618,7 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
} }
std::lock_guard<std::mutex> lock(lock_); std::lock_guard<std::mutex> lock(lock_);
if (!_on_data) { if (!_on_data || !capture_requested_) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return; 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); 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), _on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height), static_cast<int>(height), stream_id.c_str());
display_id_name_map_[current_display_].c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
} }
+2
View File
@@ -15,6 +15,8 @@ namespace crossdesk {
class ScreenCapturer { class ScreenCapturer {
public: 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*)> typedef std::function<void(unsigned char*, int, int, int, const char*)>
cb_desktop_data; cb_desktop_data;
@@ -5,6 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result; return result;
} }
std::string CleanDisplayName(const std::wstring& wide_name) { std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name); std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(), constexpr char kDevicePrefix[] = "\\\\.\\";
[](unsigned char c) { return !std::isalnum(c); }), if (name.rfind(kDevicePrefix, 0) == 0) {
name.end()); name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name; return name;
} }
} // namespace } // namespace
@@ -207,7 +209,7 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
if (FAILED(output->GetDesc(&desc))) { if (FAILED(output->GetDesc(&desc))) {
continue; continue;
} }
std::string name = CleanDisplayName(desc.DeviceName); std::string name = GetDisplayLabel(desc.DeviceName);
MONITORINFOEX mi{}; MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX); mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) { if (GetMonitorInfo(desc.Monitor, &mi)) {
@@ -380,8 +382,9 @@ void ScreenCapturerDxgi::CaptureLoop() {
if (callback_) { if (callback_) {
int idx = monitor_index_.load(); int idx = monitor_index_.load();
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) { 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, callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[idx].name.c_str()); stream_id.c_str());
} else { } else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})", LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size()); idx, display_info_list_.size());
@@ -5,6 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result; return result;
} }
std::string CleanDisplayName(const std::wstring& wide_name) { std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name); std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(), constexpr char kDevicePrefix[] = "\\\\.\\";
[](unsigned char c) { return !std::isalnum(c); }), if (name.rfind(kDevicePrefix, 0) == 0) {
name.end()); name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name; return name;
} }
} // namespace } // namespace
@@ -42,7 +44,7 @@ BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
MONITORINFOEX mi{}; MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX); mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hMonitor, &mi)) { 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; bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left, DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom); mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
@@ -209,7 +211,8 @@ void ScreenCapturerGdi::CaptureLoop() {
width, height); width, height);
if (callback_) { 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); SelectObject(mem_dc, old);
@@ -7,6 +7,7 @@
#include <iostream> #include <iostream>
#include "display_stream_id.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -24,11 +25,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result; return result;
} }
std::string CleanDisplayName(const std::wstring& wide_name) { std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name); std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(), constexpr char kDevicePrefix[] = "\\\\.\\";
[](unsigned char c) { return !std::isalnum(c); }), if (name.rfind(kDevicePrefix, 0) == 0) {
name.end()); name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name; return name;
} }
@@ -38,7 +40,7 @@ BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
monitor_info_.cbSize = sizeof(MONITORINFOEX); monitor_info_.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hmonitor, &monitor_info_)) { 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) { if (monitor_info_.dwFlags & MONITORINFOF_PRIMARY) {
gs_display_list.insert( gs_display_list.insert(
gs_display_list.begin(), 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), (uint8_t*)(nv12_frame_ + even_width * even_height),
even_width, 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, 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 <utility>
#include <vector> #include <vector>
#include "display_stream_id.h"
#include "interactive_state.h" #include "interactive_state.h"
#include "rd_log.h" #include "rd_log.h"
#include "screen_capturer_dxgi.h" #include "screen_capturer_dxgi.h"
@@ -403,37 +404,37 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps; fps_ = fps;
cb_orig_ = cb; cb_orig_ = cb;
cb_ = [this](unsigned char* data, int size, int w, int h, 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)) { if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
return; return;
} }
const char* raw_display_name = display_name ? display_name : ""; const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
std::string mapped_name; std::string mapped_stream_id;
{ {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(raw_display_name); auto it = stream_id_alias_.find(raw_stream_id);
if (it != label_alias_.end()) if (it != stream_id_alias_.end()) {
mapped_name = it->second; mapped_stream_id = it->second;
else } else {
mapped_name = raw_display_name; // 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(
std::lock_guard<std::mutex> lock(alias_mutex_); mapped_stream_id.c_str(), canonical_displays_.size(),
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) { monitor_index_.load(std::memory_order_relaxed));
if (post_secure_desktop_waiting_for_frame_.load( }
std::memory_order_relaxed) && if (mapped_stream_id.empty()) {
!post_secure_desktop_drop_logged_.exchange( if (!invalid_stream_id_logged_.exchange(true,
true, std::memory_order_relaxed)) { std::memory_order_relaxed)) {
LOG_WARN( LOG_WARN("Windows capturer dropping frame without a registered stream "
"Windows capturer dropping post-secure-desktop frame from " "id: reported='{}', size={}x{}, bytes={}",
"unknown display: display='{}', mapped='{}', size={}x{}, " raw_stream_id, w, h, size);
"bytes={}",
raw_display_name, mapped_name, w, h, size);
} }
return; return;
} }
} invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
if (post_secure_desktop_waiting_for_frame_.exchange( if (post_secure_desktop_waiting_for_frame_.exchange(
false, std::memory_order_relaxed)) { false, std::memory_order_relaxed)) {
const ULONGLONG start_tick = const ULONGLONG start_tick =
@@ -445,10 +446,11 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
std::memory_order_relaxed); std::memory_order_relaxed);
LOG_INFO( LOG_INFO(
"Windows capturer first normal frame after secure desktop: " "Windows capturer first normal frame after secure desktop: "
"display='{}', mapped='{}', size={}x{}, bytes={}, elapsed_ms={}", "reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
raw_display_name, mapped_name, w, h, size, elapsed_ms); "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; int ret = -1;
@@ -508,9 +510,8 @@ int ScreenCapturerWin::Destroy() {
} }
{ {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear(); stream_id_alias_.clear();
handle_to_canonical_.clear(); handle_to_canonical_index_.clear();
canonical_labels_.clear();
} }
return 0; return 0;
} }
@@ -523,10 +524,26 @@ int ScreenCapturerWin::Start(bool show_cursor) {
show_cursor_.store(show_cursor, std::memory_order_relaxed); show_cursor_.store(show_cursor, std::memory_order_relaxed);
paused_.store(false, 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) { 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 { auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
int r = cand->Init(fps_, cb_); int r = cand->Init(fps_, cb_);
@@ -536,6 +553,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
impl_ = std::move(cand); impl_ = std::move(cand);
impl_is_wgc_plugin_ = false; impl_is_wgc_plugin_ = false;
RebuildAliasesFromImpl(); RebuildAliasesFromImpl();
if (requested_monitor > 0 &&
impl_->SwitchTo(requested_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
return true; return true;
} }
return false; return false;
@@ -624,25 +645,49 @@ int ScreenCapturerWin::ResetToInitialMonitor() {
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() { std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
if (!impl_) return {}; if (!impl_) return {};
return impl_->GetDisplayInfoList(); std::lock_guard<std::mutex> lock(alias_mutex_);
return canonical_displays_;
} }
void ScreenCapturerWin::BuildCanonicalFromImpl() { void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_.clear(); handle_to_canonical_index_.clear();
label_alias_.clear(); stream_id_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList(); canonical_displays_ = impl_->GetDisplayInfoList();
canonical_labels_.clear(); std::unordered_map<std::string, size_t> name_counts;
for (const auto& di : canonical_displays_) { for (const auto& display : canonical_displays_) {
handle_to_canonical_[di.handle] = di.name; if (!display.name.empty()) {
canonical_labels_.insert(di.name); ++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() { void ScreenCapturerWin::RebuildAliasesFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear(); stream_id_alias_.clear();
auto current = impl_->GetDisplayInfoList(); 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) { auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
int dl = std::abs(a.left - b.left); int dl = std::abs(a.left - b.left);
int dt = std::abs(a.top - b.top); int dt = std::abs(a.top - b.top);
@@ -650,21 +695,52 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
int dh = std::abs(a.height - b.height); int dh = std::abs(a.height - b.height);
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20; return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
}; };
for (const auto& di : current) { std::vector<bool> used(canonical_displays_.size(), false);
std::string canonical; for (size_t current_index = 0; current_index < current.size();
auto it = handle_to_canonical_.find(di.handle); ++current_index) {
if (it != handle_to_canonical_.end()) { const auto& di = current[current_index];
canonical = it->second; int canonical_index = -1;
} else { auto old_handle = previous_handles.find(di.handle);
for (const auto& c : canonical_displays_) { if (old_handle != previous_handles.end() &&
if (similar(di, c) || (di.is_primary && c.is_primary)) { old_handle->second < canonical_displays_.size() &&
canonical = c.name; !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; break;
} }
} }
} }
if (!canonical.empty() && canonical != di.name) { if (canonical_index < 0 && current_index < canonical_displays_.size() &&
label_alias_[di.name] = canonical; !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; *top = display.top;
*width = capture_width; *width = capture_width;
*height = capture_height; *height = capture_height;
*display_name = display.name; *display_name = MakeDisplayStreamId(static_cast<size_t>(current_monitor));
return true; return true;
} }
@@ -10,12 +10,12 @@
#include <Windows.h> #include <Windows.h>
#include <atomic> #include <atomic>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector> #include <vector>
#include "screen_capturer.h" #include "screen_capturer.h"
@@ -48,11 +48,11 @@ class ScreenCapturerWin : public ScreenCapturer {
cb_desktop_data cb_; cb_desktop_data cb_;
cb_desktop_data cb_orig_; cb_desktop_data cb_orig_;
std::unordered_map<void*, std::string> handle_to_canonical_; std::unordered_map<void*, size_t> handle_to_canonical_index_;
std::unordered_map<std::string, std::string> label_alias_; std::unordered_map<std::string, std::string> stream_id_alias_;
std::mutex alias_mutex_; std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_; 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> running_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true}; std::atomic<bool> show_cursor_{true};