mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-24 03:59:14 +08:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b5535ab3d | ||
|
|
078bd1e520 | ||
|
|
718705c352 | ||
|
|
1ca5852280 | ||
|
|
1eda6cc183 | ||
|
|
8d37f8f5fb | ||
|
|
05b8aa629e | ||
|
|
2cbceba953 | ||
|
|
89d3c8b53f | ||
|
|
51a4190e97 | ||
|
|
3737cd0dd5 | ||
|
|
00957d6740 | ||
|
|
6ec86322b2 | ||
|
|
ecc131b5ee | ||
|
|
1aa50e9ffa | ||
|
|
d49f5bc520 | ||
|
|
8818b9b8be | ||
|
|
3be283238c | ||
|
|
5d0453fff3 | ||
|
|
8a9775a571 |
@@ -0,0 +1,59 @@
|
|||||||
|
#ifndef CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||||
|
#define CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// MiniRTC stream identifiers are protocol-facing logical identifiers. Keep
|
||||||
|
// them independent from platform display handles and user-visible names, both
|
||||||
|
// of which may change or contain duplicate/Unicode text.
|
||||||
|
inline std::string MakeDisplayStreamId(size_t display_index) {
|
||||||
|
return "Display" + std::to_string(display_index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsRegisteredDisplayStreamId(const std::string& stream_id,
|
||||||
|
size_t display_count) {
|
||||||
|
if (stream_id.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t index = 0; index < display_count; ++index) {
|
||||||
|
if (stream_id == MakeDisplayStreamId(index)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves a backend-reported identifier to a stream registered with MiniRTC.
|
||||||
|
// Backends should report MakeDisplayStreamId(index); the fallbacks keep older
|
||||||
|
// capture plugins and hotplug transitions safe.
|
||||||
|
inline std::string ResolveDisplayStreamId(
|
||||||
|
const char* reported_id, size_t display_count, int preferred_index = -1,
|
||||||
|
const std::string& previous_id = {}) {
|
||||||
|
const std::string candidate = reported_id ? reported_id : "";
|
||||||
|
if (IsRegisteredDisplayStreamId(candidate, display_count)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferred_index >= 0 &&
|
||||||
|
preferred_index < static_cast<int>(display_count)) {
|
||||||
|
return MakeDisplayStreamId(static_cast<size_t>(preferred_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsRegisteredDisplayStreamId(previous_id, display_count)) {
|
||||||
|
return previous_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_count == 1) {
|
||||||
|
return MakeDisplayStreamId(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_DISPLAY_STREAM_ID_H_
|
||||||
@@ -29,7 +29,11 @@ int ConfigCenter::Load() {
|
|||||||
return -1;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,28 +1366,58 @@ 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_);
|
{
|
||||||
password_change_pending_ = false;
|
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||||
pending_password_change_request_id_.clear();
|
password_change_pending_ = false;
|
||||||
pending_local_password_.clear();
|
pending_password_change_request_id_.clear();
|
||||||
offline_warning_text_ =
|
pending_local_password_.clear();
|
||||||
localization::signal_disconnected[localization_language_index_];
|
offline_warning_text_ =
|
||||||
show_offline_warning_window_ = true;
|
localization::signal_disconnected[localization_language_index_];
|
||||||
|
show_offline_warning_window_ = true;
|
||||||
|
}
|
||||||
|
if (!settings_.ClearPendingPasswordChange()) {
|
||||||
|
LOG_WARN("Could not clear unsent pending password change");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -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) {
|
||||||
LOG_WARN("Password change failed: {}", error);
|
if (uncertain) {
|
||||||
|
LOG_WARN("Password change outcome is unknown: {}; re-authenticating",
|
||||||
|
error);
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Password change failed: {}", error);
|
||||||
|
if (!settings_.ClearPendingPasswordChange()) {
|
||||||
|
LOG_WARN("Could not clear rejected pending password change");
|
||||||
|
}
|
||||||
|
}
|
||||||
offline_warning_text_ = localization::failed[localization_language_index_];
|
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_);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
CopyString(cache_v1_.client_id_with_password,
|
||||||
std::ios::binary);
|
owner_.client_id_with_password_);
|
||||||
if (cache_v1_file) {
|
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||||
CopyString(cache_v1_.client_id_with_password,
|
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||||
owner_.client_id_with_password_);
|
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
|
||||||
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
&cache_v1_, sizeof(cache_v1_))) {
|
||||||
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
LOG_WARN("Failed to update legacy v1 credential cache");
|
||||||
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_),
|
|
||||||
sizeof(cache_v1_));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -116,6 +116,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 +138,21 @@ 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);
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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
@@ -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,18 +814,76 @@ 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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if root.shortcut-menu-open: Rectangle {
|
if root.shortcut-menu-open: Rectangle {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display_info_list_.empty()) {
|
{
|
||||||
LOG_ERROR("Cannot start capturer: display info not initialized");
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
return -1;
|
if (display_info_list_.empty()) {
|
||||||
|
LOG_ERROR("Cannot start capturer: display info not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
show_cursor_ = show_cursor;
|
||||||
|
capture_requested_ = true;
|
||||||
|
invalid_stream_id_logged_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
show_cursor_ = show_cursor;
|
|
||||||
StartOrReconfigureCapturer();
|
StartOrReconfigureCapturer();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||||
auto display_it = display_id_map_.find(monitor_index);
|
bool should_reconfigure = false;
|
||||||
if (display_it == display_id_map_.end()) {
|
|
||||||
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
|
||||||
monitor_index, display_id_map_.size());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CGDirectDisplayID target_display = display_it->second;
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(lock_);
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
current_display_ = target_display;
|
auto display_it = display_id_map_.find(monitor_index);
|
||||||
|
if (display_it == display_id_map_.end() || monitor_index < 0 ||
|
||||||
|
monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||||
|
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||||
|
monitor_index, display_id_map_.size());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
current_monitor_index_ = monitor_index;
|
||||||
|
current_display_ = display_it->second;
|
||||||
|
current_stream_id_ = MakeDisplayStreamId(monitor_index);
|
||||||
|
should_reconfigure = capture_requested_;
|
||||||
|
}
|
||||||
|
if (should_reconfigure) {
|
||||||
|
StartOrReconfigureCapturer();
|
||||||
}
|
}
|
||||||
StartOrReconfigureCapturer();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||||
int target = initial_monitor_index_;
|
const int target = initial_monitor_index_;
|
||||||
if (display_info_list_.empty()) return -1;
|
bool should_reconfigure = false;
|
||||||
auto display_it = display_id_map_.find(target);
|
|
||||||
if (display_it == display_id_map_.end()) {
|
|
||||||
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGDirectDisplayID target_display = display_it->second;
|
|
||||||
if (current_display_ == target_display) return 0;
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(lock_);
|
std::lock_guard<std::mutex> lock(lock_);
|
||||||
|
if (display_info_list_.empty()) return -1;
|
||||||
|
auto display_it = display_id_map_.find(target);
|
||||||
|
if (display_it == display_id_map_.end()) {
|
||||||
|
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const CGDirectDisplayID target_display = display_it->second;
|
||||||
|
if (current_display_ == target_display &&
|
||||||
|
current_monitor_index_ == target) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
current_monitor_index_ = target;
|
||||||
current_display_ = target_display;
|
current_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
|
||||||
if (captured_display) {
|
: content.displays.firstObject;
|
||||||
current_display_ = captured_display.displayID;
|
if (!logical_fallback) logical_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captured_display) {
|
||||||
|
const CGDirectDisplayID old_display = current_display_;
|
||||||
|
const CGDirectDisplayID new_display = captured_display.displayID;
|
||||||
|
const std::string stable_stream_id = ResolveDisplayStreamId(
|
||||||
|
current_stream_id_.c_str(), display_info_list_.size(), logical_index);
|
||||||
|
if (stable_stream_id.empty()) {
|
||||||
|
LOG_ERROR("Cannot map macOS display {} to a registered video stream",
|
||||||
|
new_display);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_monitor_index_ = logical_index;
|
||||||
|
current_display_ = new_display;
|
||||||
|
current_stream_id_ = stable_stream_id;
|
||||||
|
display_id_map_[logical_index] = new_display;
|
||||||
|
|
||||||
|
CGRect bounds = CGDisplayBounds(new_display);
|
||||||
|
auto& info = display_info_list_[logical_index];
|
||||||
|
info.handle = (void *)(uintptr_t)new_display;
|
||||||
|
info.is_primary = CGDisplayIsMain(new_display);
|
||||||
|
info.left = static_cast<int>(bounds.origin.x);
|
||||||
|
info.top = static_cast<int>(bounds.origin.y);
|
||||||
|
info.right = static_cast<int>(bounds.origin.x + bounds.size.width);
|
||||||
|
info.bottom = static_cast<int>(bounds.origin.y + bounds.size.height);
|
||||||
|
info.width = info.right - info.left;
|
||||||
|
info.height = info.bottom - info.top;
|
||||||
|
const std::string refreshed_name = GetDisplayName(new_display);
|
||||||
|
if (!refreshed_name.empty()) {
|
||||||
|
info.name = refreshed_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_display != new_display) {
|
||||||
|
LOG_INFO("macOS display mapping refreshed: slot={}, old_id={}, "
|
||||||
|
"new_id={}, stream='{}'",
|
||||||
|
logical_index, old_display, new_display, stable_stream_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
show_cursor = show_cursor_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!captured_display) {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
|
||||||
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
|
|
||||||
if (post_secure_desktop_waiting_for_frame_.load(
|
|
||||||
std::memory_order_relaxed) &&
|
|
||||||
!post_secure_desktop_drop_logged_.exchange(
|
|
||||||
true, std::memory_order_relaxed)) {
|
|
||||||
LOG_WARN(
|
|
||||||
"Windows capturer dropping post-secure-desktop frame from "
|
|
||||||
"unknown display: display='{}', mapped='{}', size={}x{}, "
|
|
||||||
"bytes={}",
|
|
||||||
raw_display_name, mapped_name, w, h, size);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
mapped_stream_id = ResolveDisplayStreamId(
|
||||||
|
mapped_stream_id.c_str(), canonical_displays_.size(),
|
||||||
|
monitor_index_.load(std::memory_order_relaxed));
|
||||||
}
|
}
|
||||||
|
if (mapped_stream_id.empty()) {
|
||||||
|
if (!invalid_stream_id_logged_.exchange(true,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
LOG_WARN("Windows capturer dropping frame without a registered stream "
|
||||||
|
"id: reported='{}', size={}x{}, bytes={}",
|
||||||
|
raw_stream_id, w, h, size);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||||
if (post_secure_desktop_waiting_for_frame_.exchange(
|
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};
|
||||||
|
|||||||
+1
-1
Submodule submodules/minirtc updated: f49cd83129...77c5caed30
Reference in New Issue
Block a user