[fix] keep display stream IDs stable after monitor changes

This commit is contained in:
dijunkun
2026-08-24 00:32:43 +08:00
parent 078bd1e520
commit 6b5535ab3d
23 changed files with 727 additions and 231 deletions
+59
View File
@@ -0,0 +1,59 @@
#ifndef CROSSDESK_DISPLAY_STREAM_ID_H_
#define CROSSDESK_DISPLAY_STREAM_ID_H_
#include <cstddef>
#include <string>
namespace crossdesk {
// MiniRTC stream identifiers are protocol-facing logical identifiers. Keep
// them independent from platform display handles and user-visible names, both
// of which may change or contain duplicate/Unicode text.
inline std::string MakeDisplayStreamId(size_t display_index) {
return "Display" + std::to_string(display_index + 1);
}
inline bool IsRegisteredDisplayStreamId(const std::string& stream_id,
size_t display_count) {
if (stream_id.empty()) {
return false;
}
for (size_t index = 0; index < display_count; ++index) {
if (stream_id == MakeDisplayStreamId(index)) {
return true;
}
}
return false;
}
// Resolves a backend-reported identifier to a stream registered with MiniRTC.
// Backends should report MakeDisplayStreamId(index); the fallbacks keep older
// capture plugins and hotplug transitions safe.
inline std::string ResolveDisplayStreamId(
const char* reported_id, size_t display_count, int preferred_index = -1,
const std::string& previous_id = {}) {
const std::string candidate = reported_id ? reported_id : "";
if (IsRegisteredDisplayStreamId(candidate, display_count)) {
return candidate;
}
if (preferred_index >= 0 &&
preferred_index < static_cast<int>(display_count)) {
return MakeDisplayStreamId(static_cast<size_t>(preferred_index));
}
if (IsRegisteredDisplayStreamId(previous_id, display_count)) {
return previous_id;
}
if (display_count == 1) {
return MakeDisplayStreamId(0);
}
return {};
}
} // namespace crossdesk
#endif // CROSSDESK_DISPLAY_STREAM_ID_H_
+4 -2
View File
@@ -2614,8 +2614,10 @@ void GuiApplication::SyncStreamWindow() {
std::vector<slint::SharedString> displays;
displays.reserve(props->display_info_list_.size());
for (const auto& display : props->display_info_list_) {
displays.emplace_back(UiText(display.name));
for (size_t index = 0; index < props->display_info_list_.size(); ++index) {
displays.emplace_back(UiText(localization::FormatDisplayLabel(
index, props->display_info_list_[index].name,
localization_language_index_)));
}
ui_->display_model->set_vector(std::move(displays));
(*ui_->stream)->set_selected_display(props->selected_display_);
+21 -6
View File
@@ -6,6 +6,7 @@
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
@@ -110,12 +111,26 @@ inline const std::string& GetTranslatedText(const std::string& key,
} // namespace detail
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
inline std::string FormatDisplayLabel(size_t display_index,
const std::string& display_name,
int language_index) {
const std::string number = std::to_string(display_index + 1);
std::string label =
detail::GetTranslatedText("display_screen", language_index) + " " +
number;
const std::string fallback_name = "Display" + number;
if (!display_name.empty() && display_name != fallback_name) {
label += " (" + display_name + ")";
}
return label;
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
@@ -35,6 +35,7 @@ struct TranslationRow {
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(display_screen, u8"显示屏", "Display", u8"Экран") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
@@ -3,6 +3,7 @@
#include <chrono>
#include <cstdlib>
#include "display_stream_id.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
@@ -47,6 +48,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
? 30
: 60;
LOG_INFO("Init screen capturer with {} fps", fps);
display_info_list_.clear();
registered_display_stream_count_ = 0;
last_video_frame_stream_id_.clear();
invalid_video_stream_id_logged_ = false;
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
@@ -80,7 +85,21 @@ int SessionDeviceManager::InitializeScreenCapturer() {
return;
}
const std::string stream_id = display_name ? display_name : "";
const std::string stream_id = ResolveDisplayStreamId(
display_name, registered_display_stream_count_, -1,
last_video_frame_stream_id_);
if (stream_id.empty()) {
if (!invalid_video_stream_id_logged_) {
LOG_ERROR(
"Drop captured frames with an empty or unregistered video "
"stream id, reported='{}', registered_streams={}",
display_name ? display_name : "",
registered_display_stream_count_);
invalid_video_stream_id_logged_ = true;
}
return;
}
invalid_video_stream_id_logged_ = false;
const bool resumed_after_gap =
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
@@ -113,6 +132,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
registered_display_stream_count_ = display_info_list_.size();
return 0;
}
@@ -77,10 +77,12 @@ private:
MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_;
uint64_t last_frame_time_ = 0;
std::string last_video_frame_stream_id_;
bool invalid_video_stream_id_logged_ = false;
};
} // namespace crossdesk
+5 -2
View File
@@ -8,6 +8,7 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
@@ -381,8 +382,10 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
return -1;
}
for (const auto& display_info : devices_.display_info_list()) {
AddVideoStream(props->peer_, display_info.name.c_str());
const auto& displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index);
AddVideoStream(props->peer_, stream_id.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
+5 -2
View File
@@ -10,6 +10,7 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include "localization.h"
#include "rd_log.h"
@@ -181,8 +182,10 @@ int GuiRuntime::CreateConnectionPeer() {
}
if (0 == devices_.InitializeScreenCapturer()) {
for (const auto &display_info : devices_.display_info_list()) {
AddVideoStream(peer_, display_info.name.c_str());
const auto &displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index);
AddVideoStream(peer_, stream_id.c_str());
}
AddAudioStream(peer_, audio_label_.c_str());
+134 -15
View File
@@ -167,6 +167,8 @@ export component StreamWindow inherits Window {
private property <bool> control-expanded: true;
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
private property <bool> shortcut-menu-open: false;
private property <bool> control-docked-left: true;
private property <bool> control-dragging: false;
@@ -183,6 +185,48 @@ export component StreamWindow inherits Window {
callback begin-control-drag(length, length);
callback move-control-drag(length, length);
callback end-control-drag;
pure callback is-local-control-area(length, length) -> bool;
animate display-menu-progress {
duration: 120ms;
easing: ease-out;
enabled: root.display-menu-animation-enabled;
}
display-menu-open-timer := Timer {
interval: 16ms;
running: false;
triggered => {
self.running = false;
if root.display-menu-open {
root.display-menu-animation-enabled = true;
root.display-menu-progress = 1.0;
}
}
}
is-local-control-area(pointer-x, pointer-y) => {
let over-control = pointer-x >= control.x
&& pointer-x <= control.x + control.width
&& pointer-y >= control.y
&& pointer-y <= control.y + control.height;
let display-menu-x = control.x + (root.control-docked-left ? 9px : 42px);
let display-menu-y = control.y + control.height - 1px;
let display-menu-height = min(180px, root.displays.length * 30px + 8px);
let over-display-menu = root.display-menu-open
&& pointer-x >= display-menu-x
&& pointer-x <= display-menu-x + 180px
&& pointer-y >= display-menu-y
&& pointer-y <= display-menu-y + display-menu-height;
let shortcut-menu-x = control.x + (root.control-docked-left ? 41px : 74px);
let shortcut-menu-y = control.y + 40px;
let over-shortcut-menu = root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x
&& pointer-x <= shortcut-menu-x + 150px
&& pointer-y >= shortcut-menu-y
&& pointer-y <= shortcut-menu-y + 68px;
return over-control || over-display-menu || over-shortcut-menu;
}
begin-control-drag(press-x, press-y) => {
// Capture the snapped position before switching x to control-drag-x.
@@ -195,6 +239,9 @@ export component StreamWindow inherits Window {
// pointer positions remain relative to the stationary video surface.
root.control-press-x = press-x - current-x;
root.control-press-y = press-y - current-y;
display-menu-open-timer.running = false;
root.display-menu-animation-enabled = false;
root.display-menu-progress = 0.0;
root.display-menu-open = false;
root.shortcut-menu-open = false;
root.control-dragging = true;
@@ -503,7 +550,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
let over-local-ui = root.is-local-control-area(
self.mouse-x, self.mouse-y);
if event.kind == PointerEventKind.down && !over-local-ui {
input-focus.focus();
}
if event.kind == PointerEventKind.down
@@ -515,9 +564,9 @@ export component StreamWindow inherits Window {
|| event.kind == PointerEventKind.cancel) {
root.end-control-drag();
// The control bar overlays the remote video. Events in
// its full rectangle (including gaps between buttons)
// it and its attached menus (including padding and gaps)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-control {
} else if !root.control-dragging && !over-local-ui {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
}
}
@@ -527,10 +576,7 @@ export component StreamWindow inherits Window {
}
}
scroll-event(event) => {
if self.mouse-x < control.x
|| self.mouse-x > control.x + control.width
|| self.mouse-y < control.y
|| self.mouse-y > control.y + control.height {
if !root.is-local-control-area(self.mouse-x, self.mouse-y) {
root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y);
}
accept
@@ -565,12 +611,27 @@ export component StreamWindow inherits Window {
icon: FontAwesomeIcons.display;
badge: root.selected-display + 1;
tooltip: StreamStrings.select-display;
clicked => { root.display-menu-open = !root.display-menu-open; root.shortcut-menu-open = false; }
clicked => {
display-menu-open-timer.running = false;
root.display-menu-animation-enabled = false;
root.display-menu-progress = 0.0;
root.display-menu-open = !root.display-menu-open;
root.shortcut-menu-open = false;
if root.display-menu-open {
display-menu-open-timer.running = true;
}
}
}
if root.control-expanded: shortcut-button := ControlBarButton {
x: root.control-docked-left ? 41px : 74px; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
clicked => { root.shortcut-menu-open = !root.shortcut-menu-open; root.display-menu-open = false; }
clicked => {
display-menu-open-timer.running = false;
root.display-menu-animation-enabled = false;
root.display-menu-progress = 0.0;
root.shortcut-menu-open = !root.shortcut-menu-open;
root.display-menu-open = false;
}
}
if root.control-expanded: mouse-button := ControlBarButton {
x: root.control-docked-left ? 73px : 106px; y: 7px;
@@ -753,18 +814,76 @@ export component StreamWindow inherits Window {
}
}
if root.display-menu-open: Rectangle {
x: control.x + (root.control-docked-left ? 9px : 42px); y: control.y + 40px; width: 180px; height: min(180px, root.displays.length * 30px + 8px);
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
if root.display-menu-open: display-menu := Rectangle {
private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
private property <int> hovered-index:
display-touch.has-hover && display-touch.mouse-y >= 4px
&& floor((display-touch.mouse-y - 4px) / 30px) < root.displays.length
? floor((display-touch.mouse-y - 4px) / 30px) : -1;
x: control.x + (root.control-docked-left ? 9px : 42px);
// Overlap the control border by one pixel so the menu reads as
// an attached panel growing downward from the display button.
y: control.y + control.height - 1px;
width: 180px;
height: self.expanded-height * root.display-menu-progress;
opacity: root.display-menu-progress;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 5px;
clip: true;
z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
for display[index] in root.displays: Rectangle {
height: 29px;
background: display-touch.has-hover || index == root.selected-display ? #e8eef9 : transparent;
Text { x: 8px; text: display; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
display-touch := TouchArea { clicked => { root.selected-display = index; root.switch-display(index); root.display-menu-open = false; } }
background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2
: index == display-menu.hovered-index ? #d8e6f8
: index == root.selected-display ? #e8eef9
: transparent;
border-radius: 3px;
Text {
x: 8px;
width: parent.width - 34px;
text: display;
color: #30343b;
font-size: ImGuiFontStyle.body;
overflow: elide;
vertical-alignment: center;
}
if index == root.selected-display: Text {
x: parent.width - 24px;
width: 16px;
text: FontAwesomeIcons.check;
color: #2463c7;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// Use one hit target for the whole popup. A single owner keeps
// hover stable while still consuming padding, gaps and scroll
// events locally instead of forwarding them to the peer.
display-touch := TouchArea {
clicked => {
let index = floor((self.mouse-y - 4px) / 30px);
let row-y = self.mouse-y - 4px - index * 30px;
if self.mouse-y >= 4px && row-y < 29px
&& index >= 0 && index < root.displays.length {
display-menu-open-timer.running = false;
root.display-menu-animation-enabled = false;
root.display-menu-progress = 0.0;
root.selected-display = index;
root.switch-display(index);
root.display-menu-open = false;
}
}
scroll-event(event) => { accept }
}
}
if root.shortcut-menu-open: Rectangle {
+4 -1
View File
@@ -108,7 +108,10 @@ int GuiApplication::ControlBar(
if (ImGui::BeginPopup("display")) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < props->display_info_list_.size(); i++) {
if (ImGui::Selectable(props->display_info_list_[i].name.c_str())) {
const std::string display_label = localization::FormatDisplayLabel(
static_cast<size_t>(i), props->display_info_list_[i].name,
localization_language_index_);
if (ImGui::Selectable(display_label.c_str())) {
props->selected_display_ = i;
RemoteAction remote_action;
@@ -26,6 +26,7 @@
#include <chrono>
#include <thread>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -262,6 +263,7 @@ bool ScreenCapturerDrm::DiscoverOutputs() {
output.height = static_cast<int>(crtc->height);
output.name = std::string(ConnectorTypeName(connector->connector_type)) +
std::to_string(connector->connector_type_id);
output.stream_id = MakeDisplayStreamId(outputs_.size());
outputs_.push_back(output);
display_info_list_.push_back(
@@ -427,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
capture_height, output.name.c_str());
capture_height, output.stream_id.c_str());
}
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
@@ -47,6 +47,7 @@ class ScreenCapturerDrm : public ScreenCapturer {
uint32_t connector_id = 0;
uint32_t crtc_id = 0;
std::string name;
std::string stream_id;
int left = 0;
int top = 0;
int width = 0;
@@ -6,6 +6,7 @@
#include <string>
#include <utility>
#include "display_stream_id.h"
#include "platform.h"
#include "rd_log.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
@@ -49,10 +50,21 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height,
const char* display_name) {
const std::string mapped_name = MapDisplayName(display_name);
const char* reported_stream_id) {
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
if (mapped_stream_id.empty()) {
if (!invalid_stream_id_logged_.exchange(true,
std::memory_order_relaxed)) {
LOG_WARN("Linux capturer dropping frame without a registered stream "
"id: reported='{}', size={}x{}, bytes={}",
reported_stream_id ? reported_stream_id : "", width, height,
size);
}
return;
}
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
if (callback_orig_) {
callback_orig_(data, size, width, height, mapped_name.c_str());
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
}
};
@@ -139,10 +151,12 @@ int ScreenCapturerLinux::Destroy() {
backend_ = BackendType::kNone;
callback_ = nullptr;
callback_orig_ = nullptr;
current_monitor_index_.store(0, std::memory_order_relaxed);
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(alias_mutex_);
canonical_displays_.clear();
label_alias_.clear();
stream_id_alias_.clear();
}
return 0;
}
@@ -153,15 +167,12 @@ int ScreenCapturerLinux::Start(bool show_cursor) {
return -1;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (backend_ == BackendType::kWayland) {
const int refresh_ret = RefreshWaylandBackend();
if (refresh_ret != 0) {
LOG_WARN("Linux screen capturer Wayland backend refresh failed: {}",
refresh_ret);
}
// X11 output names, DRM connectors and Wayland stream handles may all change
// after an HDMI hotplug, so rebuild the current backend before each session.
const int refresh_ret = RefreshCurrentBackend();
if (refresh_ret != 0) {
LOG_WARN("Linux screen capturer backend refresh failed: {}", refresh_ret);
}
#endif
const int ret = impl_->Start(show_cursor);
if (ret == 0) {
@@ -235,14 +246,23 @@ int ScreenCapturerLinux::SwitchTo(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->SwitchTo(monitor_index);
const int ret = impl_->SwitchTo(monitor_index);
if (ret == 0) {
current_monitor_index_.store(monitor_index, std::memory_order_relaxed);
}
return ret;
}
int ScreenCapturerLinux::ResetToInitialMonitor() {
if (!impl_) {
return -1;
}
return impl_->ResetToInitialMonitor();
const int ret = impl_->ResetToInitialMonitor();
if (ret == 0) {
current_monitor_index_.store(initial_monitor_index_,
std::memory_order_relaxed);
}
return ret;
}
std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
@@ -320,27 +340,55 @@ int ScreenCapturerLinux::InitWayland() {
#endif
}
int ScreenCapturerLinux::RefreshWaylandBackend() {
int ScreenCapturerLinux::RefreshCurrentBackend() {
std::unique_ptr<ScreenCapturer> backend;
const char* backend_name = "unknown";
switch (backend_) {
case BackendType::kX11:
backend = std::make_unique<ScreenCapturerX11>();
backend_name = "X11";
break;
case BackendType::kDrm:
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
backend = std::make_unique<ScreenCapturerDrm>();
backend_name = "DRM";
break;
#else
return -1;
#endif
case BackendType::kWayland:
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto backend = std::make_unique<ScreenCapturerWayland>();
backend = std::make_unique<ScreenCapturerWayland>();
backend_name = "Wayland";
break;
#else
return -1;
#endif
case BackendType::kNone:
return -1;
}
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
return ret;
}
const int requested_monitor =
current_monitor_index_.load(std::memory_order_relaxed);
UpdateAliasesFromBackend(backend.get());
if (requested_monitor > 0 && backend->SwitchTo(requested_monitor) != 0) {
current_monitor_index_.store(0, std::memory_order_relaxed);
}
if (impl_) {
impl_->Destroy();
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer Wayland backend refreshed before start");
LOG_INFO("Linux screen capturer {} backend refreshed before start",
backend_name);
return 0;
#else
return -1;
#endif
}
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
@@ -447,61 +495,82 @@ void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
}
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
stream_id_alias_.clear();
if (canonical_displays_.empty()) {
canonical_displays_ = backend_displays;
for (const auto& display : backend_displays) {
label_alias_[display.name] = display.name;
for (size_t i = 0; i < backend_displays.size(); ++i) {
auto& canonical = canonical_displays_[i];
const std::string stream_id = MakeDisplayStreamId(i);
if (canonical.name.empty()) {
canonical.name = stream_id;
}
stream_id_alias_[stream_id] = stream_id;
}
return;
}
if (canonical_displays_.size() < backend_displays.size()) {
for (size_t i = canonical_displays_.size(); i < backend_displays.size();
++i) {
canonical_displays_.push_back(backend_displays[i]);
}
LOG_WARN("Linux capturer detected {} additional display(s); they remain "
"unavailable until peer streams are reinitialized",
backend_displays.size() - canonical_displays_.size());
}
for (size_t i = 0; i < backend_displays.size(); ++i) {
const std::string mapped_name = i < canonical_displays_.size()
? canonical_displays_[i].name
: backend_displays[i].name;
label_alias_[backend_displays[i].name] = mapped_name;
auto similar = [](const DisplayInfo& current, const DisplayInfo& canonical) {
return std::abs(current.left - canonical.left) <= 10 &&
std::abs(current.top - canonical.top) <= 10 &&
std::abs(current.width - canonical.width) <= 20 &&
std::abs(current.height - canonical.height) <= 20;
};
std::vector<bool> used(canonical_displays_.size(), false);
for (size_t current_index = 0; current_index < backend_displays.size();
++current_index) {
const auto& backend_display = backend_displays[current_index];
int canonical_index = -1;
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
if (!used[i] && similar(backend_display, canonical_displays_[i])) {
canonical_index = static_cast<int>(i);
break;
}
}
if (canonical_index < 0 && current_index < canonical_displays_.size() &&
!used[current_index]) {
canonical_index = static_cast<int>(current_index);
}
if (canonical_index < 0) {
continue;
}
if (i < canonical_displays_.size()) {
// Keep original stable names, but refresh geometry from active backend.
canonical_displays_[i].handle = backend_displays[i].handle;
canonical_displays_[i].is_primary = backend_displays[i].is_primary;
canonical_displays_[i].left = backend_displays[i].left;
canonical_displays_[i].top = backend_displays[i].top;
canonical_displays_[i].right = backend_displays[i].right;
canonical_displays_[i].bottom = backend_displays[i].bottom;
canonical_displays_[i].width = backend_displays[i].width;
canonical_displays_[i].height = backend_displays[i].height;
used[canonical_index] = true;
const std::string backend_stream_id =
MakeDisplayStreamId(current_index);
const std::string stable_stream_id =
MakeDisplayStreamId(static_cast<size_t>(canonical_index));
stream_id_alias_[backend_stream_id] = stable_stream_id;
// Refresh the user-visible label and geometry. The MiniRTC stream ID is
// derived independently from this stable logical index.
canonical_displays_[canonical_index] = backend_display;
if (canonical_displays_[canonical_index].name.empty()) {
canonical_displays_[canonical_index].name = stable_stream_id;
}
}
}
std::string ScreenCapturerLinux::MapDisplayName(
const char* display_name) const {
std::string input_name = display_name ? display_name : "";
if (input_name.empty()) {
return input_name;
}
std::string ScreenCapturerLinux::MapStreamId(
const char* reported_stream_id) const {
std::string input_id = reported_stream_id ? reported_stream_id : "";
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(input_name);
if (it != label_alias_.end()) {
return it->second;
auto it = stream_id_alias_.find(input_id);
if (it != stream_id_alias_.end()) {
input_id = it->second;
} else {
input_id.clear();
}
if (canonical_displays_.size() == 1) {
return canonical_displays_[0].name;
}
return input_name;
return ResolveDisplayStreamId(
input_id.c_str(), canonical_displays_.size(),
current_monitor_index_.load(std::memory_order_relaxed));
}
} // namespace crossdesk
@@ -7,6 +7,7 @@
#ifndef _SCREEN_CAPTURER_LINUX_H_
#define _SCREEN_CAPTURER_LINUX_H_
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
@@ -43,12 +44,12 @@ class ScreenCapturerLinux : public ScreenCapturer {
int InitX11();
int InitDrm();
int InitWayland();
int RefreshWaylandBackend();
int RefreshCurrentBackend();
bool TryFallbackToDrm(bool show_cursor);
bool TryFallbackToX11(bool show_cursor);
bool TryFallbackToWayland(bool show_cursor);
void UpdateAliasesFromBackend(ScreenCapturer* backend);
std::string MapDisplayName(const char* display_name) const;
std::string MapStreamId(const char* reported_stream_id) const;
private:
std::unique_ptr<ScreenCapturer> impl_;
@@ -58,7 +59,10 @@ class ScreenCapturerLinux : public ScreenCapturer {
cb_desktop_data callback_orig_;
std::vector<DisplayInfo> canonical_displays_;
mutable std::mutex alias_mutex_;
std::unordered_map<std::string, std::string> label_alias_;
std::unordered_map<std::string, std::string> stream_id_alias_;
std::atomic<int> current_monitor_index_{0};
int initial_monitor_index_ = 0;
mutable std::atomic<bool> invalid_stream_id_logged_{false};
};
} // namespace crossdesk
@@ -15,6 +15,7 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -831,8 +832,9 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(0);
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
even_height, display_name_.c_str());
even_height, stream_id.c_str());
}
pipewire_last_frame_ms_.store(NowMs());
@@ -10,6 +10,7 @@
#include <mutex>
#include <thread>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -97,12 +98,6 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
name = "Display" + std::to_string(i + 1);
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
display_info_list_.push_back(DisplayInfo(
(void*)display_, name, true, crtc_info->x, crtc_info->y,
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height));
@@ -324,8 +319,9 @@ void ScreenCapturerX11::OnFrame() {
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(monitor_index);
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
display_info_list_[monitor_index].name.c_str());
stream_id.c_str());
}
XDestroyImage(image);
@@ -10,21 +10,21 @@
#include "screen_capturer_sck.h"
#include <AppKit/AppKit.h>
#include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/graphics/IOGraphicsLib.h>
#include <IOSurface/IOSurface.h>
#include <ScreenCaptureKit/ScreenCaptureKit.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cstring>
#include <limits>
#include <map>
#include <mutex>
#include <vector>
#include "display_info.h"
#include "display_stream_id.h"
#include "rd_log.h"
using namespace crossdesk;
@@ -83,20 +83,23 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int Resume(int monitor_index) override { return 0; }
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
std::vector<DisplayInfo> GetDisplayInfoList() override {
std::lock_guard<std::mutex> lock(lock_);
return display_info_list_;
}
int ResetToInitialMonitor() override;
private:
std::vector<DisplayInfo> display_info_list_;
std::map<int, CGDirectDisplayID> display_id_map_;
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0;
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool show_cursor_ = false;
bool capture_requested_ = false;
bool invalid_stream_id_logged_ = false;
public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
@@ -130,9 +133,38 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0;
int initial_monitor_index_ = 0;
int current_monitor_index_ = 0;
std::string current_stream_id_;
};
static std::string NSStringToUtf8(NSString *value) {
if (!value || value.length == 0) return "";
const char *utf8 = value.UTF8String;
return utf8 ? utf8 : "";
}
static std::string GetAppKitDisplayName(CGDirectDisplayID display_id) {
if (@available(macOS 10.15, *)) {
@autoreleasepool {
for (NSScreen *screen in NSScreen.screens) {
NSNumber *screen_number = screen.deviceDescription[@"NSScreenNumber"];
if (screen_number && screen_number.unsignedIntValue == display_id) {
return NSStringToUtf8(screen.localizedName);
}
}
}
}
return "";
}
std::string GetDisplayName(CGDirectDisplayID display_id) {
// IODisplayConnect is no longer exposed for some external displays on
// modern Apple Silicon Macs. NSScreen is the system-owned source for the
// user-visible name and maps directly to CGDirectDisplayID.
std::string appkit_name = GetAppKitDisplayName(display_id);
if (!appkit_name.empty()) return appkit_name;
// Keep the IOKit path for older macOS versions and display drivers.
io_iterator_t iter;
io_service_t serv = 0, matched_serv = 0;
@@ -212,8 +244,6 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
display_info_list_.clear();
display_id_map_.clear();
display_id_map_reverse_.clear();
display_id_name_map_.clear();
if (nv12_frame_) {
delete[] nv12_frame_;
@@ -232,8 +262,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
fps_ = fps > 0 ? fps : 60;
display_info_list_.clear();
display_id_map_.clear();
display_id_map_reverse_.clear();
display_id_name_map_.clear();
if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess();
@@ -265,26 +293,18 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
return -1;
}
CGDirectDisplayID displays[10];
uint32_t count;
CGGetActiveDisplayList(10, displays, &count);
int unnamed_count = 1;
for (SCDisplay *display in content.displays) {
CGDirectDisplayID display_id = display.displayID;
CGRect bounds = CGDisplayBounds(display_id);
bool is_primary = CGDisplayIsMain(display_id);
std::string name = GetDisplayName(display_id);
if (name.empty()) {
name = "Display" + std::to_string(unnamed_count++);
name = MakeDisplayStreamId(display_info_list_.size());
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(), [](unsigned char c) { return !std::isalnum(c); }),
name.end());
LOG_INFO("macOS display discovered: index={}, display_id={}, name='{}'",
display_info_list_.size(), display_id, name);
DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary,
static_cast<int>(bounds.origin.x), static_cast<int>(bounds.origin.y),
@@ -293,11 +313,12 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
display_info_list_.push_back(info);
display_id_map_[display_info_list_.size() - 1] = display_id;
display_id_map_reverse_[display_id] = display_info_list_.size() - 1;
display_id_name_map_[display_id] = name;
}
initial_monitor_index_ = 0;
current_monitor_index_ = initial_monitor_index_;
current_display_ = display_id_map_[current_monitor_index_];
current_stream_id_ = MakeDisplayStreamId(current_monitor_index_);
return 0;
}
@@ -307,49 +328,62 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
return -1;
}
if (display_info_list_.empty()) {
LOG_ERROR("Cannot start capturer: display info not initialized");
return -1;
{
std::lock_guard<std::mutex> lock(lock_);
if (display_info_list_.empty()) {
LOG_ERROR("Cannot start capturer: display info not initialized");
return -1;
}
show_cursor_ = show_cursor;
capture_requested_ = true;
invalid_stream_id_logged_ = false;
}
show_cursor_ = show_cursor;
StartOrReconfigureCapturer();
return 0;
}
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
auto display_it = display_id_map_.find(monitor_index);
if (display_it == display_id_map_.end()) {
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
monitor_index, display_id_map_.size());
return -1;
}
const CGDirectDisplayID target_display = display_it->second;
bool should_reconfigure = false;
{
std::lock_guard<std::mutex> lock(lock_);
current_display_ = target_display;
auto display_it = display_id_map_.find(monitor_index);
if (display_it == display_id_map_.end() || monitor_index < 0 ||
monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
monitor_index, display_id_map_.size());
return -1;
}
current_monitor_index_ = monitor_index;
current_display_ = display_it->second;
current_stream_id_ = MakeDisplayStreamId(monitor_index);
should_reconfigure = capture_requested_;
}
if (should_reconfigure) {
StartOrReconfigureCapturer();
}
StartOrReconfigureCapturer();
return 0;
}
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
const int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1;
auto display_it = display_id_map_.find(target);
if (display_it == display_id_map_.end()) {
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
return -1;
}
const CGDirectDisplayID target_display = display_it->second;
bool should_reconfigure = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (current_display_ == target_display) return 0;
if (display_info_list_.empty()) return -1;
auto display_it = display_id_map_.find(target);
if (display_it == display_id_map_.end()) {
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
return -1;
}
const CGDirectDisplayID target_display = display_it->second;
if (current_display_ == target_display &&
current_monitor_index_ == target) {
return 0;
}
current_monitor_index_ = target;
current_display_ = target_display;
should_reconfigure = stream_ != nil;
current_stream_id_ = MakeDisplayStreamId(target);
should_reconfigure = capture_requested_ && stream_ != nil;
}
// Resetting session state must not create a capture stream. Preserve the
@@ -369,7 +403,9 @@ int ScreenCapturerSckImpl::Destroy() {
[stream_ stopCaptureWithCompletionHandler:nil];
stream_ = nil;
}
capture_requested_ = false;
current_display_ = 0;
current_stream_id_.clear();
permanent_error_ = false;
_on_data = nullptr;
helper_to_release = helper_;
@@ -383,6 +419,7 @@ int ScreenCapturerSckImpl::Destroy() {
int ScreenCapturerSckImpl::Stop() {
std::lock_guard<std::mutex> lock(lock_);
capture_requested_ = false;
if (stream_) {
LOG_INFO("Stopping stream");
[stream_ stopCaptureWithCompletionHandler:nil];
@@ -394,6 +431,14 @@ int ScreenCapturerSckImpl::Stop() {
}
void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *content) {
{
std::lock_guard<std::mutex> lock(lock_);
if (!capture_requested_) {
LOG_INFO("Ignoring stale ScreenCaptureKit display refresh after stop");
return;
}
}
if (!content) {
LOG_ERROR("getShareableContent failed");
permanent_error_ = true;
@@ -407,22 +452,71 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
}
SCDisplay *captured_display = nil;
bool show_cursor = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (!capture_requested_) return;
int logical_index = current_monitor_index_;
if (logical_index < 0 ||
logical_index >= static_cast<int>(display_info_list_.size())) {
logical_index = 0;
}
SCDisplay *logical_fallback = nil;
int display_index = 0;
for (SCDisplay *display in content.displays) {
if (display_index == logical_index) logical_fallback = display;
if (current_display_ != 0 && current_display_ == display.displayID) {
LOG_WARN("current display: {}, name: {}", current_display_,
display_id_name_map_[current_display_]);
captured_display = display;
break;
}
++display_index;
}
if (!captured_display) {
captured_display = content.displays.firstObject;
if (captured_display) {
current_display_ = captured_display.displayID;
captured_display = logical_fallback ? logical_fallback
: content.displays.firstObject;
if (!logical_fallback) logical_index = 0;
}
if (captured_display) {
const CGDirectDisplayID old_display = current_display_;
const CGDirectDisplayID new_display = captured_display.displayID;
const std::string stable_stream_id = ResolveDisplayStreamId(
current_stream_id_.c_str(), display_info_list_.size(), logical_index);
if (stable_stream_id.empty()) {
LOG_ERROR("Cannot map macOS display {} to a registered video stream",
new_display);
return;
}
current_monitor_index_ = logical_index;
current_display_ = new_display;
current_stream_id_ = stable_stream_id;
display_id_map_[logical_index] = new_display;
CGRect bounds = CGDisplayBounds(new_display);
auto& info = display_info_list_[logical_index];
info.handle = (void *)(uintptr_t)new_display;
info.is_primary = CGDisplayIsMain(new_display);
info.left = static_cast<int>(bounds.origin.x);
info.top = static_cast<int>(bounds.origin.y);
info.right = static_cast<int>(bounds.origin.x + bounds.size.width);
info.bottom = static_cast<int>(bounds.origin.y + bounds.size.height);
info.width = info.right - info.left;
info.height = info.bottom - info.top;
const std::string refreshed_name = GetDisplayName(new_display);
if (!refreshed_name.empty()) {
info.name = refreshed_name;
}
if (old_display != new_display) {
LOG_INFO("macOS display mapping refreshed: slot={}, old_id={}, "
"new_id={}, stream='{}'",
logical_index, old_display, new_display, stable_stream_id);
}
}
show_cursor = show_cursor_;
}
if (!captured_display) {
@@ -447,13 +541,14 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
}
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
config.showsCursor = show_cursor_;
config.showsCursor = show_cursor;
config.width = filter.contentRect.size.width * filter.pointPixelScale;
config.height = filter.contentRect.size.height * filter.pointPixelScale;
config.captureResolution = SCCaptureResolutionAutomatic;
config.minimumFrameInterval = CMTimeMake(1, fps_);
std::lock_guard<std::mutex> lock(lock_);
if (!capture_requested_) return;
if (stream_) {
LOG_INFO("Updating stream configuration");
@@ -523,7 +618,7 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
}
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data) {
if (!_on_data || !capture_requested_) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
@@ -558,9 +653,22 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
}
const std::string stream_id = ResolveDisplayStreamId(
current_stream_id_.c_str(), display_info_list_.size(),
current_monitor_index_);
if (stream_id.empty()) {
if (!invalid_stream_id_logged_) {
LOG_ERROR("Dropping macOS frames without a registered stream id, "
"display_id={}",
current_display_);
invalid_stream_id_logged_ = true;
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
invalid_stream_id_logged_ = false;
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height),
display_id_name_map_[current_display_].c_str());
static_cast<int>(height), stream_id.c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
}
+3 -1
View File
@@ -15,6 +15,8 @@ namespace crossdesk {
class ScreenCapturer {
public:
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
// not a platform display name or physical handle.
typedef std::function<void(unsigned char*, int, int, int, const char*)>
cb_desktop_data;
@@ -34,4 +36,4 @@ class ScreenCapturer {
virtual int ResetToInitialMonitor() = 0;
};
} // namespace crossdesk
#endif
#endif
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
} // namespace
@@ -207,7 +209,7 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
if (FAILED(output->GetDesc(&desc))) {
continue;
}
std::string name = CleanDisplayName(desc.DeviceName);
std::string name = GetDisplayLabel(desc.DeviceName);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) {
@@ -380,8 +382,9 @@ void ScreenCapturerDxgi::CaptureLoop() {
if (callback_) {
int idx = monitor_index_.load();
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[idx].name.c_str());
stream_id.c_str());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -21,11 +22,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
} // namespace
@@ -42,7 +44,7 @@ BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hMonitor, &mi)) {
std::string name = CleanDisplayName(mi.szDevice);
std::string name = GetDisplayLabel(mi.szDevice);
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
@@ -209,7 +211,8 @@ void ScreenCapturerGdi::CaptureLoop() {
width, height);
if (callback_) {
callback_(nv12_frame_, nv12_size, width, height, di.name.c_str());
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
}
SelectObject(mem_dc, old);
@@ -221,4 +224,4 @@ void ScreenCapturerGdi::CaptureLoop() {
ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk
} // namespace crossdesk
@@ -7,6 +7,7 @@
#include <iostream>
#include "display_stream_id.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -24,11 +25,12 @@ std::string WideToUtf8(const std::wstring& wstr) {
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
@@ -38,7 +40,7 @@ BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
monitor_info_.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hmonitor, &monitor_info_)) {
std::string display_name = CleanDisplayName(monitor_info_.szDevice);
std::string display_name = GetDisplayLabel(monitor_info_.szDevice);
if (monitor_info_.dwFlags & MONITORINFOF_PRIMARY) {
gs_display_list.insert(
gs_display_list.begin(),
@@ -402,8 +404,9 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
(uint8_t*)(nv12_frame_ + even_width * even_height),
even_width, even_width, even_height);
const std::string stream_id = MakeDisplayStreamId(id);
on_data_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[id].name.c_str());
stream_id.c_str());
}
}
@@ -14,6 +14,7 @@
#include <utility>
#include <vector>
#include "display_stream_id.h"
#include "interactive_state.h"
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
@@ -403,37 +404,37 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
cb_orig_ = cb;
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* display_name) {
const char* reported_stream_id) {
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
return;
}
const char* raw_display_name = display_name ? display_name : "";
std::string mapped_name;
const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
std::string mapped_stream_id;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(raw_display_name);
if (it != label_alias_.end())
mapped_name = it->second;
else
mapped_name = raw_display_name;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
if (post_secure_desktop_waiting_for_frame_.load(
std::memory_order_relaxed) &&
!post_secure_desktop_drop_logged_.exchange(
true, std::memory_order_relaxed)) {
LOG_WARN(
"Windows capturer dropping post-secure-desktop frame from "
"unknown display: display='{}', mapped='{}', size={}x{}, "
"bytes={}",
raw_display_name, mapped_name, w, h, size);
}
return;
auto it = stream_id_alias_.find(raw_stream_id);
if (it != stream_id_alias_.end()) {
mapped_stream_id = it->second;
} else {
// Unknown backend labels are presentation data, not protocol IDs.
// Resolve them through the selected logical display instead.
mapped_stream_id.clear();
}
mapped_stream_id = ResolveDisplayStreamId(
mapped_stream_id.c_str(), canonical_displays_.size(),
monitor_index_.load(std::memory_order_relaxed));
}
if (mapped_stream_id.empty()) {
if (!invalid_stream_id_logged_.exchange(true,
std::memory_order_relaxed)) {
LOG_WARN("Windows capturer dropping frame without a registered stream "
"id: reported='{}', size={}x{}, bytes={}",
raw_stream_id, w, h, size);
}
return;
}
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
if (post_secure_desktop_waiting_for_frame_.exchange(
false, std::memory_order_relaxed)) {
const ULONGLONG start_tick =
@@ -445,10 +446,11 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
std::memory_order_relaxed);
LOG_INFO(
"Windows capturer first normal frame after secure desktop: "
"display='{}', mapped='{}', size={}x{}, bytes={}, elapsed_ms={}",
raw_display_name, mapped_name, w, h, size, elapsed_ms);
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
"elapsed_ms={}",
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
}
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
};
int ret = -1;
@@ -508,9 +510,8 @@ int ScreenCapturerWin::Destroy() {
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
handle_to_canonical_.clear();
canonical_labels_.clear();
stream_id_alias_.clear();
handle_to_canonical_index_.clear();
}
return 0;
}
@@ -523,10 +524,26 @@ int ScreenCapturerWin::Start(bool show_cursor) {
show_cursor_.store(show_cursor, std::memory_order_relaxed);
paused_.store(false, std::memory_order_relaxed);
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
int ret = impl_->Start(show_cursor);
// Refresh physical monitor identities before every session. HMONITOR and
// DXGI output handles may change after an HDMI hotplug while CrossDesk stays
// open; logical stream IDs must remain stable.
const int requested_monitor =
monitor_index_.load(std::memory_order_relaxed);
impl_->Destroy();
int ret = impl_->Init(fps_, cb_);
if (ret == 0) {
RebuildAliasesFromImpl();
ret = impl_->Start(show_cursor);
if (ret == 0 && requested_monitor > 0 &&
impl_->SwitchTo(requested_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
}
if (ret != 0) {
LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret);
LOG_WARN("Windows capturer: refresh/start failed (ret={}), trying fallback",
ret);
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
int r = cand->Init(fps_, cb_);
@@ -536,6 +553,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
impl_ = std::move(cand);
impl_is_wgc_plugin_ = false;
RebuildAliasesFromImpl();
if (requested_monitor > 0 &&
impl_->SwitchTo(requested_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
return true;
}
return false;
@@ -624,25 +645,49 @@ int ScreenCapturerWin::ResetToInitialMonitor() {
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
if (!impl_) return {};
return impl_->GetDisplayInfoList();
std::lock_guard<std::mutex> lock(alias_mutex_);
return canonical_displays_;
}
void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_.clear();
label_alias_.clear();
handle_to_canonical_index_.clear();
stream_id_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList();
canonical_labels_.clear();
for (const auto& di : canonical_displays_) {
handle_to_canonical_[di.handle] = di.name;
canonical_labels_.insert(di.name);
std::unordered_map<std::string, size_t> name_counts;
for (const auto& display : canonical_displays_) {
if (!display.name.empty()) {
++name_counts[display.name];
}
}
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
auto& di = canonical_displays_[i];
const std::string stream_id = MakeDisplayStreamId(i);
if (di.name.empty()) {
di.name = stream_id;
}
handle_to_canonical_index_[di.handle] = i;
stream_id_alias_[stream_id] = stream_id;
if (name_counts[di.name] == 1) {
// Compatibility with an older WGC plugin that reports display names.
stream_id_alias_[di.name] = stream_id;
}
}
}
void ScreenCapturerWin::RebuildAliasesFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
stream_id_alias_.clear();
auto current = impl_->GetDisplayInfoList();
if (current.empty() || canonical_displays_.empty()) return;
const auto previous_handles = handle_to_canonical_index_;
handle_to_canonical_index_.clear();
std::unordered_map<std::string, size_t> name_counts;
for (const auto& display : current) {
if (!display.name.empty()) {
++name_counts[display.name];
}
}
auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
int dl = std::abs(a.left - b.left);
int dt = std::abs(a.top - b.top);
@@ -650,21 +695,52 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
int dh = std::abs(a.height - b.height);
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
};
for (const auto& di : current) {
std::string canonical;
auto it = handle_to_canonical_.find(di.handle);
if (it != handle_to_canonical_.end()) {
canonical = it->second;
} else {
for (const auto& c : canonical_displays_) {
if (similar(di, c) || (di.is_primary && c.is_primary)) {
canonical = c.name;
std::vector<bool> used(canonical_displays_.size(), false);
for (size_t current_index = 0; current_index < current.size();
++current_index) {
const auto& di = current[current_index];
int canonical_index = -1;
auto old_handle = previous_handles.find(di.handle);
if (old_handle != previous_handles.end() &&
old_handle->second < canonical_displays_.size() &&
!used[old_handle->second]) {
canonical_index = static_cast<int>(old_handle->second);
}
if (canonical_index < 0) {
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
if (!used[i] && (similar(di, canonical_displays_[i]) ||
(di.is_primary && canonical_displays_[i].is_primary))) {
canonical_index = static_cast<int>(i);
break;
}
}
}
if (!canonical.empty() && canonical != di.name) {
label_alias_[di.name] = canonical;
if (canonical_index < 0 && current_index < canonical_displays_.size() &&
!used[current_index]) {
canonical_index = static_cast<int>(current_index);
}
if (canonical_index < 0) {
LOG_WARN("Windows capturer ignoring unregistered display after topology "
"change: display='{}', index={}",
di.name, current_index);
continue;
}
used[canonical_index] = true;
auto& canonical = canonical_displays_[canonical_index];
const std::string backend_stream_id =
MakeDisplayStreamId(current_index);
const std::string stable_stream_id =
MakeDisplayStreamId(static_cast<size_t>(canonical_index));
stream_id_alias_[backend_stream_id] = stable_stream_id;
if (!di.name.empty() && name_counts[di.name] == 1) {
stream_id_alias_[di.name] = stable_stream_id;
}
handle_to_canonical_index_[di.handle] =
static_cast<size_t>(canonical_index);
canonical = di;
if (canonical.name.empty()) {
canonical.name = stable_stream_id;
}
}
}
@@ -792,7 +868,7 @@ bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
*top = display.top;
*width = capture_width;
*height = capture_height;
*display_name = display.name;
*display_name = MakeDisplayStreamId(static_cast<size_t>(current_monitor));
return true;
}
@@ -10,12 +10,12 @@
#include <Windows.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "screen_capturer.h"
@@ -48,11 +48,11 @@ class ScreenCapturerWin : public ScreenCapturer {
cb_desktop_data cb_;
cb_desktop_data cb_orig_;
std::unordered_map<void*, std::string> handle_to_canonical_;
std::unordered_map<std::string, std::string> label_alias_;
std::unordered_map<void*, size_t> handle_to_canonical_index_;
std::unordered_map<std::string, std::string> stream_id_alias_;
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::unordered_set<std::string> canonical_labels_;
std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};