From 511831ced38f229ed67b2f80eef198239822d1bc Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 23 Mar 2026 05:18:56 +0800 Subject: [PATCH] [fix] fix Wayland reconnect black screen by keeping capturer warm and also fix Wayland mouse control --- src/common/platform.cpp | 26 +- src/common/platform.h | 3 +- src/common/wayland_portal_shared.cpp | 279 +++++ src/common/wayland_portal_shared.h | 37 + .../mouse/linux/mouse_controller.cpp | 73 +- .../mouse/linux/mouse_controller.h | 37 +- .../mouse/linux/mouse_controller_wayland.cpp | 1075 +++++++++++++++++ src/gui/render.cpp | 159 ++- src/gui/render.h | 1 + src/gui/render_callback.cpp | 138 ++- .../linux/screen_capturer_linux.cpp | 67 +- .../linux/screen_capturer_linux.h | 1 + .../linux/screen_capturer_wayland.cpp | 119 +- .../linux/screen_capturer_wayland.h | 13 +- .../linux/screen_capturer_wayland_build.h | 16 + .../screen_capturer_wayland_pipewire.cpp | 350 +++++- .../linux/screen_capturer_wayland_portal.cpp | 227 +++- 17 files changed, 2418 insertions(+), 203 deletions(-) create mode 100644 src/common/wayland_portal_shared.cpp create mode 100644 src/common/wayland_portal_shared.h create mode 100644 src/device_controller/mouse/linux/mouse_controller_wayland.cpp diff --git a/src/common/platform.cpp b/src/common/platform.cpp index 26cd875..d0c2dea 100644 --- a/src/common/platform.cpp +++ b/src/common/platform.cpp @@ -1,5 +1,8 @@ #include "platform.h" +#include +#include + #include "rd_log.h" #ifdef _WIN32 @@ -125,4 +128,25 @@ std::string GetHostName() { #endif return hostname; } -} // namespace crossdesk \ No newline at end of file + +bool IsWaylandSession() { +#if defined(__linux__) && !defined(__APPLE__) + const char* session_type = std::getenv("XDG_SESSION_TYPE"); + if (session_type) { + if (std::strcmp(session_type, "wayland") == 0 || + std::strcmp(session_type, "Wayland") == 0) { + return true; + } + if (std::strcmp(session_type, "x11") == 0 || + std::strcmp(session_type, "X11") == 0) { + return false; + } + } + + const char* wayland_display = std::getenv("WAYLAND_DISPLAY"); + return wayland_display && wayland_display[0] != '\0'; +#else + return false; +#endif +} +} // namespace crossdesk diff --git a/src/common/platform.h b/src/common/platform.h index e83af47..dee6cc0 100644 --- a/src/common/platform.h +++ b/src/common/platform.h @@ -13,6 +13,7 @@ namespace crossdesk { std::string GetMac(); std::string GetHostName(); +bool IsWaylandSession(); } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/src/common/wayland_portal_shared.cpp b/src/common/wayland_portal_shared.cpp new file mode 100644 index 0000000..e3197fd --- /dev/null +++ b/src/common/wayland_portal_shared.cpp @@ -0,0 +1,279 @@ +#include "wayland_portal_shared.h" + +#include +#include + +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER +#include +#endif + +#include "rd_log.h" + +namespace crossdesk { + +namespace { + +std::mutex& SharedSessionMutex() { + static std::mutex mutex; + return mutex; +} + +SharedWaylandPortalSessionInfo& SharedSessionInfo() { + static SharedWaylandPortalSessionInfo info; + return info; +} + +bool& SharedSessionActive() { + static bool active = false; + return active; +} + +int& SharedSessionRefs() { + static int refs = 0; + return refs; +} + +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER +constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop"; +constexpr const char* kPortalSessionInterface = + "org.freedesktop.portal.Session"; +constexpr int kPortalCloseWaitMs = 100; + +void LogCloseDbusError(const char* action, DBusError* error) { + if (error && dbus_error_is_set(error)) { + LOG_ERROR("{} failed: {} ({})", action, + error->message ? error->message : "unknown", + error->name ? error->name : "unknown"); + } else { + LOG_ERROR("{} failed", action); + } +} + +struct SessionClosedState { + std::string session_handle; + bool received = false; +}; + +DBusHandlerResult HandleSessionClosedSignal(DBusConnection* connection, + DBusMessage* message, + void* user_data) { + (void)connection; + auto* state = static_cast(user_data); + if (!state || !message) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (!dbus_message_is_signal(message, kPortalSessionInterface, "Closed")) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + const char* path = dbus_message_get_path(message); + if (!path || state->session_handle != path) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + state->received = true; + return DBUS_HANDLER_RESULT_HANDLED; +} + +bool BeginSessionClosedWatch(DBusConnection* connection, + const std::string& session_handle, + SessionClosedState* state, + std::string* match_rule_out) { + if (!connection || session_handle.empty() || !state || !match_rule_out) { + return false; + } + + state->session_handle = session_handle; + state->received = false; + DBusError error; + dbus_error_init(&error); + const std::string match_rule = + "type='signal',interface='" + std::string(kPortalSessionInterface) + + "',member='Closed',path='" + session_handle + "'"; + dbus_bus_add_match(connection, match_rule.c_str(), &error); + if (dbus_error_is_set(&error)) { + LogCloseDbusError("dbus_bus_add_match(Session.Closed)", &error); + dbus_error_free(&error); + return false; + } + + dbus_connection_add_filter(connection, HandleSessionClosedSignal, state, + nullptr); + *match_rule_out = match_rule; + return true; +} + +void EndSessionClosedWatch(DBusConnection* connection, SessionClosedState* state, + const std::string& match_rule) { + if (!connection || !state || match_rule.empty()) { + return; + } + + dbus_connection_remove_filter(connection, HandleSessionClosedSignal, state); + + DBusError remove_error; + dbus_error_init(&remove_error); + dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error); + if (dbus_error_is_set(&remove_error)) { + dbus_error_free(&remove_error); + } +} + +void WaitForSessionClosed(DBusConnection* connection, SessionClosedState* state, + int timeout_ms = kPortalCloseWaitMs) { + if (!connection || !state) { + return; + } + + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); + while (!state->received && std::chrono::steady_clock::now() < deadline) { + dbus_connection_read_write(connection, 100); + while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) { + } + } +} +#endif + +} // namespace + +bool PublishSharedWaylandPortalSession( + const SharedWaylandPortalSessionInfo& info) { + if (!info.connection || info.session_handle.empty() || info.stream_id == 0) { + return false; + } + + std::lock_guard lock(SharedSessionMutex()); + if (SharedSessionActive()) { + const auto& active_info = SharedSessionInfo(); + if (active_info.session_handle != info.session_handle && + SharedSessionRefs() > 0) { + return false; + } + } + + const bool same_session = + SharedSessionActive() && + SharedSessionInfo().session_handle == info.session_handle; + SharedSessionInfo() = info; + SharedSessionActive() = true; + if (!same_session || SharedSessionRefs() <= 0) { + SharedSessionRefs() = 1; + } + return true; +} + +bool AcquireSharedWaylandPortalSession(bool require_pointer, + SharedWaylandPortalSessionInfo* out) { + if (!out) { + return false; + } + + std::lock_guard lock(SharedSessionMutex()); + if (!SharedSessionActive()) { + return false; + } + + const auto& info = SharedSessionInfo(); + if (require_pointer && !info.pointer_granted) { + return false; + } + + ++SharedSessionRefs(); + *out = info; + return true; +} + +bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out, + std::string* session_handle_out) { + if (connection_out) { + *connection_out = nullptr; + } + if (session_handle_out) { + session_handle_out->clear(); + } + + std::lock_guard lock(SharedSessionMutex()); + if (!SharedSessionActive()) { + return false; + } + + if (SharedSessionRefs() > 0) { + --SharedSessionRefs(); + } + + if (SharedSessionRefs() > 0) { + return true; + } + + if (connection_out) { + *connection_out = SharedSessionInfo().connection; + } + if (session_handle_out) { + *session_handle_out = SharedSessionInfo().session_handle; + } + + SharedSessionInfo() = SharedWaylandPortalSessionInfo{}; + SharedSessionActive() = false; + SharedSessionRefs() = 0; + return true; +} + +void CloseWaylandPortalSessionAndConnection(DBusConnection* connection, + const std::string& session_handle, + const char* close_action) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + if (!connection) { + return; + } + + if (!session_handle.empty()) { + SessionClosedState close_state; + std::string close_match_rule; + const bool watching_closed = BeginSessionClosedWatch( + connection, session_handle, &close_state, &close_match_rule); + + DBusMessage* message = dbus_message_new_method_call( + kPortalBusName, session_handle.c_str(), kPortalSessionInterface, + "Close"); + if (message) { + DBusError error; + dbus_error_init(&error); + DBusMessage* reply = dbus_connection_send_with_reply_and_block( + connection, message, 1000, &error); + if (!reply && dbus_error_is_set(&error)) { + LogCloseDbusError(close_action, &error); + dbus_error_free(&error); + } + if (reply) { + dbus_message_unref(reply); + } + dbus_message_unref(message); + } + + if (watching_closed) { + WaitForSessionClosed(connection, &close_state); + if (!close_state.received) { + LOG_WARN("Timed out waiting for portal session to close: {}", + session_handle); + LOG_WARN("Forcing local teardown without waiting for Session.Closed: {}", + session_handle); + EndSessionClosedWatch(connection, &close_state, close_match_rule); + } else { + EndSessionClosedWatch(connection, &close_state, close_match_rule); + LOG_INFO("Portal session closed: {}", session_handle); + } + } + } + + dbus_connection_close(connection); + dbus_connection_unref(connection); +#else + (void)connection; + (void)session_handle; + (void)close_action; +#endif +} + +} // namespace crossdesk diff --git a/src/common/wayland_portal_shared.h b/src/common/wayland_portal_shared.h new file mode 100644 index 0000000..492e8ca --- /dev/null +++ b/src/common/wayland_portal_shared.h @@ -0,0 +1,37 @@ +/* + * Shared Wayland portal session state used by the Linux Wayland capturer and + * mouse controller so they can reuse one RemoteDesktop session. + */ + +#ifndef _WAYLAND_PORTAL_SHARED_H_ +#define _WAYLAND_PORTAL_SHARED_H_ + +#include +#include + +struct DBusConnection; + +namespace crossdesk { + +struct SharedWaylandPortalSessionInfo { + DBusConnection* connection = nullptr; + std::string session_handle; + uint32_t stream_id = 0; + int width = 0; + int height = 0; + bool pointer_granted = false; +}; + +bool PublishSharedWaylandPortalSession( + const SharedWaylandPortalSessionInfo& info); +bool AcquireSharedWaylandPortalSession(bool require_pointer, + SharedWaylandPortalSessionInfo* out); +bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out, + std::string* session_handle_out); +void CloseWaylandPortalSessionAndConnection(DBusConnection* connection, + const std::string& session_handle, + const char* close_action); + +} // namespace crossdesk + +#endif diff --git a/src/device_controller/mouse/linux/mouse_controller.cpp b/src/device_controller/mouse/linux/mouse_controller.cpp index b9594b3..ca7af51 100644 --- a/src/device_controller/mouse/linux/mouse_controller.cpp +++ b/src/device_controller/mouse/linux/mouse_controller.cpp @@ -2,6 +2,7 @@ #include +#include "platform.h" #include "rd_log.h" namespace crossdesk { @@ -12,6 +13,17 @@ MouseController::~MouseController() { Destroy(); } int MouseController::Init(std::vector display_info_list) { display_info_list_ = display_info_list; + + if (IsWaylandSession()) { + if (InitWaylandPortal()) { + use_wayland_portal_ = true; + LOG_INFO("Mouse controller initialized with Wayland portal backend"); + return 0; + } + LOG_WARN( + "Wayland mouse control init failed, falling back to X11/XTest backend"); + } + display_ = XOpenDisplay(NULL); if (!display_) { LOG_ERROR("Cannot connect to X server"); @@ -25,26 +37,68 @@ int MouseController::Init(std::vector display_info_list) { &minor_version)) { LOG_ERROR("XTest extension not available"); XCloseDisplay(display_); + display_ = nullptr; return -2; } return 0; } +void MouseController::UpdateDisplayInfoList( + const std::vector& display_info_list) { + if (display_info_list.empty()) { + return; + } + + display_info_list_ = display_info_list; + if (use_wayland_portal_) { + OnWaylandDisplayInfoListUpdated(); + } + + if (last_display_index_ < 0 || + last_display_index_ >= static_cast(display_info_list_.size())) { + last_display_index_ = -1; + last_norm_x_ = -1.0; + last_norm_y_ = -1.0; + } +} + int MouseController::Destroy() { + CleanupWaylandPortal(); + if (display_) { XCloseDisplay(display_); display_ = nullptr; } + return 0; } int MouseController::SendMouseCommand(RemoteAction remote_action, int display_index) { + if (remote_action.type != ControlType::mouse) { + return 0; + } + + if (use_wayland_portal_) { + return SendWaylandMouseCommand(remote_action, display_index); + } + + if (!display_) { + LOG_ERROR("X11 display not initialized"); + return -1; + } + switch (remote_action.type) { case mouse: switch (remote_action.m.flag) { - case MouseFlag::move: + case MouseFlag::move: { + if (display_index < 0 || + display_index >= static_cast(display_info_list_.size())) { + LOG_ERROR("Invalid display index: {}", display_index); + return -2; + } + SetMousePosition( static_cast(remote_action.m.x * display_info_list_[display_index].width + @@ -53,6 +107,7 @@ int MouseController::SendMouseCommand(RemoteAction remote_action, display_info_list_[display_index].height + display_info_list_[display_index].top)); break; + } case MouseFlag::left_down: XTestFakeButtonEvent(display_, 1, True, CurrentTime); XFlush(display_); @@ -103,25 +158,39 @@ int MouseController::SendMouseCommand(RemoteAction remote_action, } void MouseController::SetMousePosition(int x, int y) { + if (!display_) { + return; + } XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y); XFlush(display_); } void MouseController::SimulateKeyDown(int kval) { + if (!display_) { + return; + } XTestFakeKeyEvent(display_, kval, True, CurrentTime); XFlush(display_); } void MouseController::SimulateKeyUp(int kval) { + if (!display_) { + return; + } XTestFakeKeyEvent(display_, kval, False, CurrentTime); XFlush(display_); } void MouseController::SimulateMouseWheel(int direction_button, int count) { + if (!display_) { + return; + } + for (int i = 0; i < count; ++i) { XTestFakeButtonEvent(display_, direction_button, True, CurrentTime); XTestFakeButtonEvent(display_, direction_button, False, CurrentTime); } XFlush(display_); } -} // namespace crossdesk \ No newline at end of file + +} // namespace crossdesk diff --git a/src/device_controller/mouse/linux/mouse_controller.h b/src/device_controller/mouse/linux/mouse_controller.h index 0123a92..0f1574c 100644 --- a/src/device_controller/mouse/linux/mouse_controller.h +++ b/src/device_controller/mouse/linux/mouse_controller.h @@ -11,10 +11,16 @@ #include #include +#include +#include +#include #include #include "device_controller.h" +struct DBusConnection; +struct DBusMessageIter; + namespace crossdesk { class MouseController : public DeviceController { @@ -26,18 +32,47 @@ class MouseController : public DeviceController { virtual int Init(std::vector display_info_list); virtual int Destroy(); virtual int SendMouseCommand(RemoteAction remote_action, int display_index); + void UpdateDisplayInfoList(const std::vector& display_info_list); private: void SimulateKeyDown(int kval); void SimulateKeyUp(int kval); void SetMousePosition(int x, int y); void SimulateMouseWheel(int direction_button, int count); + bool InitWaylandPortal(); + void CleanupWaylandPortal(); + int SendWaylandMouseCommand(RemoteAction remote_action, int display_index); + void OnWaylandDisplayInfoListUpdated(); + bool NotifyWaylandPointerMotion(double dx, double dy); + bool NotifyWaylandPointerMotionAbsolute(uint32_t stream, double x, double y); + bool NotifyWaylandPointerButton(int button, uint32_t state); + bool NotifyWaylandPointerAxisDiscrete(uint32_t axis, int32_t steps); + bool SendWaylandPortalVoidCall(const char* method_name, + const std::function& + append_args); + + enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled }; Display* display_ = nullptr; Window root_ = 0; std::vector display_info_list_; int screen_width_ = 0; int screen_height_ = 0; + bool use_wayland_portal_ = false; + + DBusConnection* dbus_connection_ = nullptr; + std::string wayland_session_handle_; + int last_display_index_ = -1; + double last_norm_x_ = -1.0; + double last_norm_y_ = -1.0; + bool logged_wayland_display_info_ = false; + uintptr_t last_logged_wayland_stream_ = 0; + int last_logged_wayland_width_ = 0; + int last_logged_wayland_height_ = 0; + WaylandAbsoluteMode wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown; + bool wayland_absolute_disabled_logged_ = false; + uint32_t wayland_absolute_stream_id_ = 0; + bool using_shared_wayland_session_ = false; }; } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/src/device_controller/mouse/linux/mouse_controller_wayland.cpp b/src/device_controller/mouse/linux/mouse_controller_wayland.cpp new file mode 100644 index 0000000..05fe690 --- /dev/null +++ b/src/device_controller/mouse/linux/mouse_controller_wayland.cpp @@ -0,0 +1,1075 @@ +#include "mouse_controller.h" + +#include +#include +#include +#include +#include +#include + +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER +#include +#endif + +#include "platform.h" +#include "rd_log.h" +#include "wayland_portal_shared.h" + +namespace crossdesk { + +void MouseController::OnWaylandDisplayInfoListUpdated() { + const uintptr_t stream0 = + display_info_list_.empty() + ? 0 + : reinterpret_cast(display_info_list_[0].handle); + const int width0 = display_info_list_.empty() ? 0 : display_info_list_[0].width; + const int height0 = + display_info_list_.empty() ? 0 : display_info_list_[0].height; + const bool should_log = !logged_wayland_display_info_ || + stream0 != last_logged_wayland_stream_ || + width0 != last_logged_wayland_width_ || + height0 != last_logged_wayland_height_; + + if (!should_log) { + return; + } + + logged_wayland_display_info_ = true; + last_logged_wayland_stream_ = stream0; + last_logged_wayland_width_ = width0; + last_logged_wayland_height_ = height0; + + for (size_t i = 0; i < display_info_list_.size(); ++i) { + const auto& display = display_info_list_[i]; + LOG_INFO( + "Wayland mouse display info [{}]: name={}, rect=({},{})->({},{}) " + "size={}x{}, stream={}" + , + i, display.name, display.left, display.top, display.right, + display.bottom, display.width, display.height, + reinterpret_cast(display.handle)); + } +} + +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER +namespace { + +constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop"; +constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop"; +constexpr const char* kPortalRemoteDesktopInterface = + "org.freedesktop.portal.RemoteDesktop"; +constexpr const char* kPortalScreenCastInterface = + "org.freedesktop.portal.ScreenCast"; +constexpr const char* kPortalRequestInterface = + "org.freedesktop.portal.Request"; +constexpr const char* kPortalSessionInterface = + "org.freedesktop.portal.Session"; +constexpr const char* kPortalRequestPathPrefix = + "/org/freedesktop/portal/desktop/request/"; +constexpr const char* kPortalSessionPathPrefix = + "/org/freedesktop/portal/desktop/session/"; + +constexpr uint32_t kRemoteDesktopDevicePointer = 2u; +constexpr uint32_t kScreenCastSourceMonitor = 1u; + +constexpr uint32_t kPointerReleased = 0u; +constexpr uint32_t kPointerPressed = 1u; + +constexpr uint32_t kPointerAxisVertical = 0u; +constexpr uint32_t kPointerAxisHorizontal = 1u; + +constexpr int kBtnLeft = 0x110; +constexpr int kBtnRight = 0x111; +constexpr int kBtnMiddle = 0x112; + +std::string MakeToken(const char* prefix) { + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + return std::string(prefix) + "_" + std::to_string(now); +} + +void LogDbusError(const char* action, DBusError* error) { + if (error && dbus_error_is_set(error)) { + LOG_ERROR("{} failed: {} ({})", action, + error->message ? error->message : "unknown", + error->name ? error->name : "unknown"); + } else { + LOG_ERROR("{} failed", action); + } +} + +void AppendDictEntryString(DBusMessageIter* dict, const char* key, + const std::string& value) { + DBusMessageIter entry; + DBusMessageIter variant; + const char* key_cstr = key; + const char* value_cstr = value.c_str(); + + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(dict, &entry); +} + +void AppendDictEntryUint32(DBusMessageIter* dict, const char* key, + uint32_t value) { + DBusMessageIter entry; + DBusMessageIter variant; + const char* key_cstr = key; + + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(dict, &entry); +} + +void AppendDictEntryBool(DBusMessageIter* dict, const char* key, bool value) { + DBusMessageIter entry; + DBusMessageIter variant; + const char* key_cstr = key; + dbus_bool_t bool_value = value ? TRUE : FALSE; + + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "b", &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &bool_value); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(dict, &entry); +} + +void AppendEmptyOptionsDict(DBusMessageIter* iter) { + DBusMessageIter options; + dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &options); + dbus_message_iter_close_container(iter, &options); +} + +bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) { + if (!variant || !value) { + return false; + } + + const int type = dbus_message_iter_get_arg_type(variant); + if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) { + const char* temp = nullptr; + dbus_message_iter_get_basic(variant, &temp); + if (temp && temp[0] != '\0') { + *value = temp; + return true; + } + } + + return false; +} + +bool ReadUint32Like(DBusMessageIter* iter, uint32_t* value) { + if (!iter || !value) { + return false; + } + + const int type = dbus_message_iter_get_arg_type(iter); + if (type == DBUS_TYPE_UINT32) { + uint32_t temp = 0; + dbus_message_iter_get_basic(iter, &temp); + *value = temp; + return true; + } + + if (type == DBUS_TYPE_INT32) { + int32_t temp = 0; + dbus_message_iter_get_basic(iter, &temp); + if (temp < 0) { + return false; + } + *value = static_cast(temp); + return true; + } + + return false; +} + +bool ReadFirstStreamId(DBusMessageIter* variant, uint32_t* stream_id) { + if (!variant || !stream_id) { + return false; + } + if (dbus_message_iter_get_arg_type(variant) != DBUS_TYPE_ARRAY) { + return false; + } + + DBusMessageIter streams; + dbus_message_iter_recurse(variant, &streams); + while (dbus_message_iter_get_arg_type(&streams) != DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&streams) == DBUS_TYPE_STRUCT) { + DBusMessageIter stream; + dbus_message_iter_recurse(&streams, &stream); + uint32_t candidate = 0; + if (ReadUint32Like(&stream, &candidate) && candidate != 0) { + *stream_id = candidate; + return true; + } + } + dbus_message_iter_next(&streams); + } + return false; +} + +std::string BuildSessionHandleFromRequestPath( + const std::string& request_path, const std::string& session_handle_token) { + if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 || + session_handle_token.empty()) { + return ""; + } + + const size_t sender_start = strlen(kPortalRequestPathPrefix); + const size_t token_sep = request_path.find('/', sender_start); + if (token_sep == std::string::npos || token_sep <= sender_start) { + return ""; + } + + const std::string sender = + request_path.substr(sender_start, token_sep - sender_start); + if (sender.empty()) { + return ""; + } + + return std::string(kPortalSessionPathPrefix) + sender + "/" + + session_handle_token; +} + +struct PortalResponseState { + std::string request_path; + bool received = false; + DBusMessage* message = nullptr; +}; + +DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection, + DBusMessage* message, + void* user_data) { + auto* state = static_cast(user_data); + if (!state || !message) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + const char* path = dbus_message_get_path(message); + if (!path || state->request_path != path) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (state->message) { + dbus_message_unref(state->message); + state->message = nullptr; + } + + state->message = dbus_message_ref(message); + state->received = true; + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusMessage* WaitForPortalResponse(DBusConnection* connection, + const std::string& request_path, + int timeout_ms = 120000) { + if (!connection || request_path.empty()) { + return nullptr; + } + + PortalResponseState state; + state.request_path = request_path; + + DBusError error; + dbus_error_init(&error); + + const std::string match_rule = + "type='signal',interface='" + std::string(kPortalRequestInterface) + + "',member='Response',path='" + request_path + "'"; + dbus_bus_add_match(connection, match_rule.c_str(), &error); + if (dbus_error_is_set(&error)) { + LogDbusError("dbus_bus_add_match", &error); + dbus_error_free(&error); + return nullptr; + } + + dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state, + nullptr); + + auto deadline = + std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); + while (!state.received && std::chrono::steady_clock::now() < deadline) { + dbus_connection_read_write(connection, 100); + while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) { + } + } + + dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state); + + DBusError remove_error; + dbus_error_init(&remove_error); + dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error); + if (dbus_error_is_set(&remove_error)) { + dbus_error_free(&remove_error); + } + + return state.message; +} + +bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) { + if (!reply || !request_path) { + return false; + } + + const char* path = nullptr; + DBusError error; + dbus_error_init(&error); + const dbus_bool_t ok = dbus_message_get_args( + reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); + if (!ok || !path) { + LogDbusError("dbus_message_get_args(request_path)", &error); + dbus_error_free(&error); + return false; + } + + *request_path = path; + return true; +} + +bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code, + DBusMessageIter* results_array) { + if (!message || !response_code || !results_array) { + return false; + } + + DBusMessageIter iter; + if (!dbus_message_iter_init(message, &iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) { + return false; + } + + dbus_message_iter_get_basic(&iter, response_code); + if (!dbus_message_iter_next(&iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + return false; + } + + *results_array = iter; + return true; +} + +bool SendPortalRequestAndHandleResponse( + DBusConnection* connection, const char* interface_name, + const char* method_name, + const char* action_name, + const std::function& append_message_args, + const std::function& handle_results, + std::string* request_path_out = nullptr) { + if (!connection || !interface_name || interface_name[0] == '\0' || + !method_name || method_name[0] == '\0') { + return false; + } + + DBusMessage* message = dbus_message_new_method_call( + kPortalBusName, kPortalObjectPath, interface_name, method_name); + if (!message) { + LOG_ERROR("Failed to allocate {} message", method_name); + return false; + } + + if (append_message_args && !append_message_args(message)) { + dbus_message_unref(message); + LOG_ERROR("{} arguments are malformed", method_name); + return false; + } + + DBusError error; + dbus_error_init(&error); + DBusMessage* reply = + dbus_connection_send_with_reply_and_block(connection, message, -1, &error); + dbus_message_unref(message); + if (!reply) { + LogDbusError(action_name ? action_name : method_name, &error); + dbus_error_free(&error); + return false; + } + + std::string request_path; + const bool got_request_path = ExtractRequestPath(reply, &request_path); + dbus_message_unref(reply); + if (!got_request_path) { + return false; + } + if (request_path_out) { + *request_path_out = request_path; + } + + DBusMessage* response = WaitForPortalResponse(connection, request_path); + if (!response) { + LOG_ERROR("Timed out waiting for {} response", method_name); + return false; + } + + uint32_t response_code = 1; + DBusMessageIter results; + const bool parsed = ExtractPortalResponse(response, &response_code, &results); + if (!parsed) { + dbus_message_unref(response); + LOG_ERROR("{} response was malformed", method_name); + return false; + } + + const bool ok = handle_results ? handle_results(response_code, &results) + : (response_code == 0); + dbus_message_unref(response); + return ok; +} + +} // namespace +#endif + +bool MouseController::InitWaylandPortal() { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + CleanupWaylandPortal(); + + auto attach_shared_session = + [this](const SharedWaylandPortalSessionInfo& shared_session) { + dbus_connection_ = shared_session.connection; + wayland_session_handle_ = shared_session.session_handle; + wayland_absolute_stream_id_ = shared_session.stream_id; + last_display_index_ = -1; + last_norm_x_ = -1.0; + last_norm_y_ = -1.0; + logged_wayland_display_info_ = false; + last_logged_wayland_stream_ = 0; + last_logged_wayland_width_ = 0; + last_logged_wayland_height_ = 0; + wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown; + wayland_absolute_disabled_logged_ = false; + using_shared_wayland_session_ = true; + LOG_INFO("Mouse controller attached to shared Wayland portal session, " + "stream_id={}", + wayland_absolute_stream_id_); + return true; + }; + + SharedWaylandPortalSessionInfo shared_session; + if (AcquireSharedWaylandPortalSession(true, &shared_session)) { + return attach_shared_session(shared_session); + } + + const auto wait_deadline = + std::chrono::steady_clock::now() + std::chrono::milliseconds(800); + bool waiting_logged = false; + while (std::chrono::steady_clock::now() < wait_deadline) { + if (AcquireSharedWaylandPortalSession(true, &shared_session)) { + return attach_shared_session(shared_session); + } + + if (!waiting_logged) { + waiting_logged = true; + LOG_INFO("Waiting for shared Wayland portal session from screen " + "capturer before creating a standalone mouse session"); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + if (waiting_logged) { + LOG_WARN("Shared Wayland portal session did not appear in time; falling " + "back to standalone mouse portal session"); + } + + if (AcquireSharedWaylandPortalSession(true, &shared_session)) { + return attach_shared_session(shared_session); + } + + DBusError error; + dbus_error_init(&error); + + DBusConnection* check_connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (!check_connection) { + LogDbusError("dbus_bus_get", &error); + dbus_error_free(&error); + return false; + } + + const dbus_bool_t has_owner = + dbus_bus_name_has_owner(check_connection, kPortalBusName, &error); + if (dbus_error_is_set(&error)) { + LogDbusError("dbus_bus_name_has_owner", &error); + dbus_error_free(&error); + dbus_connection_unref(check_connection); + return false; + } + dbus_connection_unref(check_connection); + + if (!has_owner) { + LOG_ERROR("xdg-desktop-portal is not available on session bus"); + return false; + } + + dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error); + if (!dbus_connection_) { + LogDbusError("dbus_bus_get_private", &error); + dbus_error_free(&error); + return false; + } + dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE); + + const std::string session_handle_token = MakeToken("crossdesk_mouse_session"); + std::string request_path; + const bool create_ok = SendPortalRequestAndHandleResponse( + dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession", + "CreateSession", + [&](DBusMessage* message) { + DBusMessageIter iter; + DBusMessageIter options; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &options); + AppendDictEntryString(&options, "session_handle_token", + session_handle_token); + AppendDictEntryString(&options, "handle_token", + MakeToken("crossdesk_mouse_req")); + dbus_message_iter_close_container(&iter, &options); + return true; + }, + [&](uint32_t response_code, DBusMessageIter* results) { + if (response_code != 0) { + LOG_ERROR("RemoteDesktop.CreateSession denied, response={}", + response_code); + return false; + } + + DBusMessageIter dict; + dbus_message_iter_recurse(results, &dict); + while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry; + dbus_message_iter_recurse(&dict, &entry); + + const char* key = nullptr; + dbus_message_iter_get_basic(&entry, &key); + if (key && dbus_message_iter_next(&entry) && + dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT && + strcmp(key, "session_handle") == 0) { + DBusMessageIter variant; + std::string parsed_handle; + dbus_message_iter_recurse(&entry, &variant); + if (ReadPathLikeVariant(&variant, &parsed_handle) && + !parsed_handle.empty()) { + wayland_session_handle_ = parsed_handle; + break; + } + } + } + dbus_message_iter_next(&dict); + } + return true; + }, + &request_path); + + if (!create_ok) { + CleanupWaylandPortal(); + return false; + } + + if (wayland_session_handle_.empty()) { + wayland_session_handle_ = + BuildSessionHandleFromRequestPath(request_path, session_handle_token); + } + + if (wayland_session_handle_.empty()) { + LOG_ERROR("RemoteDesktop.CreateSession did not return session handle"); + CleanupWaylandPortal(); + return false; + } + + const char* session_handle = wayland_session_handle_.c_str(); + const bool select_ok = SendPortalRequestAndHandleResponse( + dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices", + "SelectDevices", + [&](DBusMessage* message) { + DBusMessageIter iter; + DBusMessageIter options; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &options); + AppendDictEntryUint32(&options, "types", kRemoteDesktopDevicePointer); + AppendDictEntryString(&options, "handle_token", + MakeToken("crossdesk_mouse_req")); + dbus_message_iter_close_container(&iter, &options); + return true; + }, + [](uint32_t response_code, DBusMessageIter*) { + if (response_code != 0) { + LOG_ERROR("RemoteDesktop.SelectDevices denied, response={}", + response_code); + return false; + } + return true; + }); + + if (!select_ok) { + CleanupWaylandPortal(); + return false; + } + + const bool select_sources_ok = SendPortalRequestAndHandleResponse( + dbus_connection_, kPortalScreenCastInterface, "SelectSources", + "SelectSources", + [&](DBusMessage* message) { + DBusMessageIter iter; + DBusMessageIter options; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &options); + AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor); + AppendDictEntryBool(&options, "multiple", false); + AppendDictEntryString(&options, "handle_token", + MakeToken("crossdesk_mouse_req")); + dbus_message_iter_close_container(&iter, &options); + return true; + }, + [](uint32_t response_code, DBusMessageIter*) { + if (response_code != 0) { + LOG_ERROR("ScreenCast.SelectSources denied, response={}", + response_code); + return false; + } + return true; + }); + + if (!select_sources_ok) { + CleanupWaylandPortal(); + return false; + } + + const char* parent_window = ""; + bool pointer_granted = false; + const bool start_ok = SendPortalRequestAndHandleResponse( + dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start", + [&](DBusMessage* message) { + DBusMessageIter iter; + DBusMessageIter options; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &options); + AppendDictEntryString(&options, "handle_token", + MakeToken("crossdesk_mouse_req")); + dbus_message_iter_close_container(&iter, &options); + return true; + }, + [&](uint32_t response_code, DBusMessageIter* results) { + if (response_code != 0) { + LOG_ERROR("RemoteDesktop.Start denied, response={}", response_code); + return false; + } + + uint32_t granted_devices = 0; + uint32_t absolute_stream_id = 0; + DBusMessageIter dict; + dbus_message_iter_recurse(results, &dict); + while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry; + dbus_message_iter_recurse(&dict, &entry); + + const char* key = nullptr; + dbus_message_iter_get_basic(&entry, &key); + if (key && dbus_message_iter_next(&entry) && + dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) { + DBusMessageIter variant; + dbus_message_iter_recurse(&entry, &variant); + if (strcmp(key, "devices") == 0) { + ReadUint32Like(&variant, &granted_devices); + } else if (strcmp(key, "streams") == 0) { + ReadFirstStreamId(&variant, &absolute_stream_id); + } + } + } + dbus_message_iter_next(&dict); + } + + pointer_granted = (granted_devices & kRemoteDesktopDevicePointer) != 0; + if (!pointer_granted) { + LOG_ERROR( + "RemoteDesktop.Start granted devices mask={}, pointer not allowed", + granted_devices); + return false; + } + if (absolute_stream_id == 0) { + LOG_ERROR("RemoteDesktop.Start did not return a screencast stream id"); + return false; + } + wayland_absolute_stream_id_ = absolute_stream_id; + wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown; + wayland_absolute_disabled_logged_ = false; + LOG_INFO("Wayland mouse absolute stream id={}", + wayland_absolute_stream_id_); + return true; + }); + + if (!start_ok) { + CleanupWaylandPortal(); + return false; + } + + if (!pointer_granted) { + LOG_ERROR("RemoteDesktop session started without pointer permission"); + CleanupWaylandPortal(); + return false; + } + if (wayland_absolute_stream_id_ == 0) { + LOG_ERROR("Wayland absolute stream id is missing after Start"); + CleanupWaylandPortal(); + return false; + } + + last_display_index_ = -1; + last_norm_x_ = -1.0; + last_norm_y_ = -1.0; + logged_wayland_display_info_ = false; + last_logged_wayland_stream_ = 0; + last_logged_wayland_width_ = 0; + last_logged_wayland_height_ = 0; + wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown; + wayland_absolute_disabled_logged_ = false; + return true; +#else + return false; +#endif +} + +void MouseController::CleanupWaylandPortal() { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + if (using_shared_wayland_session_) { + DBusConnection* close_connection = nullptr; + std::string close_session_handle; + ReleaseSharedWaylandPortalSession(&close_connection, &close_session_handle); + if (close_connection) { + CloseWaylandPortalSessionAndConnection(close_connection, + close_session_handle, + "RemoteDesktop.Session.Close"); + } + dbus_connection_ = nullptr; + } else if (dbus_connection_) { + CloseWaylandPortalSessionAndConnection(dbus_connection_, + wayland_session_handle_, + "RemoteDesktop.Session.Close"); + dbus_connection_ = nullptr; + } +#endif + + use_wayland_portal_ = false; + wayland_session_handle_.clear(); + last_display_index_ = -1; + last_norm_x_ = -1.0; + last_norm_y_ = -1.0; + logged_wayland_display_info_ = false; + last_logged_wayland_stream_ = 0; + last_logged_wayland_width_ = 0; + last_logged_wayland_height_ = 0; + wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown; + wayland_absolute_disabled_logged_ = false; + wayland_absolute_stream_id_ = 0; + using_shared_wayland_session_ = false; +} + +int MouseController::SendWaylandMouseCommand(RemoteAction remote_action, + int display_index) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + if (!dbus_connection_ || wayland_session_handle_.empty()) { + return -1; + } + + switch (remote_action.m.flag) { + case MouseFlag::move: { + if (display_index < 0 || + display_index >= static_cast(display_info_list_.size())) { + LOG_ERROR("Invalid display index for Wayland mouse move: {}", + display_index); + return -2; + } + if (wayland_absolute_stream_id_ == 0) { + if (!wayland_absolute_disabled_logged_) { + wayland_absolute_disabled_logged_ = true; + LOG_ERROR( + "Wayland absolute pointer stream id is missing; cannot send " + "pointer motion"); + } + return -3; + } + if (wayland_absolute_mode_ == WaylandAbsoluteMode::kDisabled) { + if (!wayland_absolute_disabled_logged_) { + wayland_absolute_disabled_logged_ = true; + LOG_ERROR( + "Wayland absolute pointer mode is unavailable for current " + "portal session"); + } + return -3; + } + + const DisplayInfo& display_info = display_info_list_[display_index]; + const int width = std::max(display_info.width, 1); + const int height = std::max(display_info.height, 1); + + const double norm_x = + std::clamp(static_cast(remote_action.m.x), 0.0, 1.0); + const double norm_y = + std::clamp(static_cast(remote_action.m.y), 0.0, 1.0); + + if (last_display_index_ == display_index && + std::abs(norm_x - last_norm_x_) < 1e-6 && + std::abs(norm_y - last_norm_y_) < 1e-6) { + return 0; + } + + const uint32_t stream = wayland_absolute_stream_id_; + const double abs_x = norm_x * std::max(width - 1, 1); + const double abs_y = norm_y * std::max(height - 1, 1); + + auto accept_absolute = [&]() { + last_display_index_ = display_index; + last_norm_x_ = norm_x; + last_norm_y_ = norm_y; + return 0; + }; + + if (wayland_absolute_mode_ == WaylandAbsoluteMode::kPixels) { + if (NotifyWaylandPointerMotionAbsolute(stream, abs_x, abs_y)) { + return accept_absolute(); + } + wayland_absolute_mode_ = WaylandAbsoluteMode::kDisabled; + } else if (wayland_absolute_mode_ == WaylandAbsoluteMode::kNormalized) { + if (NotifyWaylandPointerMotionAbsolute(stream, norm_x, norm_y)) { + return accept_absolute(); + } + wayland_absolute_mode_ = WaylandAbsoluteMode::kDisabled; + } else { + if (NotifyWaylandPointerMotionAbsolute(stream, abs_x, abs_y)) { + wayland_absolute_mode_ = WaylandAbsoluteMode::kPixels; + LOG_INFO("Wayland absolute pointer mode selected: pixel coordinates"); + return accept_absolute(); + } + + if (NotifyWaylandPointerMotionAbsolute(stream, norm_x, norm_y)) { + wayland_absolute_mode_ = WaylandAbsoluteMode::kNormalized; + LOG_INFO( + "Wayland absolute pointer mode selected: normalized " + "coordinates"); + return accept_absolute(); + } + + wayland_absolute_mode_ = WaylandAbsoluteMode::kDisabled; + } + + if (!wayland_absolute_disabled_logged_) { + wayland_absolute_disabled_logged_ = true; + LOG_ERROR( + "NotifyPointerMotionAbsolute rejected by portal backend in both " + "pixel and normalized modes"); + } + return -3; + } + case MouseFlag::left_down: + if (!NotifyWaylandPointerButton(kBtnLeft, kPointerPressed)) { + return -3; + } + break; + case MouseFlag::left_up: + if (!NotifyWaylandPointerButton(kBtnLeft, kPointerReleased)) { + return -3; + } + break; + case MouseFlag::right_down: + if (!NotifyWaylandPointerButton(kBtnRight, kPointerPressed)) { + return -3; + } + break; + case MouseFlag::right_up: + if (!NotifyWaylandPointerButton(kBtnRight, kPointerReleased)) { + return -3; + } + break; + case MouseFlag::middle_down: + if (!NotifyWaylandPointerButton(kBtnMiddle, kPointerPressed)) { + return -3; + } + break; + case MouseFlag::middle_up: + if (!NotifyWaylandPointerButton(kBtnMiddle, kPointerReleased)) { + return -3; + } + break; + case MouseFlag::wheel_vertical: { + if (remote_action.m.s == 0) { + return 0; + } + if (!NotifyWaylandPointerAxisDiscrete(kPointerAxisVertical, + remote_action.m.s)) { + return -3; + } + break; + } + case MouseFlag::wheel_horizontal: { + if (remote_action.m.s == 0) { + return 0; + } + if (!NotifyWaylandPointerAxisDiscrete(kPointerAxisHorizontal, + remote_action.m.s)) { + return -3; + } + break; + } + } + + return 0; +#else + (void)remote_action; + (void)display_index; + return -1; +#endif +} + +bool MouseController::NotifyWaylandPointerMotion(double dx, double dy) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + return SendWaylandPortalVoidCall( + "NotifyPointerMotion", [&](DBusMessageIter* iter) { + const char* session_handle = wayland_session_handle_.c_str(); + dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + AppendEmptyOptionsDict(iter); + dbus_message_iter_append_basic(iter, DBUS_TYPE_DOUBLE, &dx); + dbus_message_iter_append_basic(iter, DBUS_TYPE_DOUBLE, &dy); + }); +#else + (void)dx; + (void)dy; + return false; +#endif +} + +bool MouseController::NotifyWaylandPointerMotionAbsolute(uint32_t stream, + double x, double y) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + return SendWaylandPortalVoidCall( + "NotifyPointerMotionAbsolute", [&](DBusMessageIter* iter) { + const char* session_handle = wayland_session_handle_.c_str(); + uint32_t stream_id = stream; + dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + AppendEmptyOptionsDict(iter); + dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &stream_id); + dbus_message_iter_append_basic(iter, DBUS_TYPE_DOUBLE, &x); + dbus_message_iter_append_basic(iter, DBUS_TYPE_DOUBLE, &y); + }); +#else + (void)stream; + (void)x; + (void)y; + return false; +#endif +} + +bool MouseController::NotifyWaylandPointerButton(int button, uint32_t state) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + return SendWaylandPortalVoidCall( + "NotifyPointerButton", [&](DBusMessageIter* iter) { + const char* session_handle = wayland_session_handle_.c_str(); + int32_t btn = button; + uint32_t btn_state = state; + dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + AppendEmptyOptionsDict(iter); + dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &btn); + dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &btn_state); + }); +#else + (void)button; + (void)state; + return false; +#endif +} + +bool MouseController::NotifyWaylandPointerAxisDiscrete(uint32_t axis, + int32_t steps) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + return SendWaylandPortalVoidCall( + "NotifyPointerAxisDiscrete", [&](DBusMessageIter* iter) { + const char* session_handle = wayland_session_handle_.c_str(); + uint32_t axis_id = axis; + int32_t discrete_steps = steps; + dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + AppendEmptyOptionsDict(iter); + dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &axis_id); + dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &discrete_steps); + }); +#else + (void)axis; + (void)steps; + return false; +#endif +} + +bool MouseController::SendWaylandPortalVoidCall( + const char* method_name, + const std::function& append_args) { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + if (!dbus_connection_ || !method_name || method_name[0] == '\0') { + return false; + } + + DBusMessage* message = dbus_message_new_method_call( + kPortalBusName, kPortalObjectPath, kPortalRemoteDesktopInterface, + method_name); + if (!message) { + LOG_ERROR("Failed to allocate {} message", method_name); + return false; + } + + DBusMessageIter iter; + dbus_message_iter_init_append(message, &iter); + if (append_args) { + append_args(&iter); + } + + DBusError error; + dbus_error_init(&error); + DBusMessage* reply = dbus_connection_send_with_reply_and_block( + dbus_connection_, message, 5000, &error); + dbus_message_unref(message); + if (!reply) { + LogDbusError(method_name, &error); + dbus_error_free(&error); + return false; + } + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + const char* error_name = dbus_message_get_error_name(reply); + LOG_ERROR("{} returned DBus error: {}", method_name, + error_name ? error_name : "unknown"); + dbus_message_unref(reply); + return false; + } + + dbus_message_unref(reply); + return true; +#else + (void)method_name; + (void)append_args; + return false; +#endif +} + +} // namespace crossdesk diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 7b56ce0..93d0833 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -8,6 +8,7 @@ #endif #include +#include #include #include #include @@ -580,8 +581,9 @@ int Render::ScreenCapturerInit() { if (0 == screen_capturer_init_ret) { LOG_INFO("Init screen capturer success"); - if (display_info_list_.empty()) { - display_info_list_ = screen_capturer_->GetDisplayInfoList(); + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; } return 0; } else { @@ -594,10 +596,22 @@ int Render::ScreenCapturerInit() { } int Render::StartScreenCapturer() { + if (!screen_capturer_) { + LOG_INFO("Screen capturer instance missing, recreating before start"); + if (0 != ScreenCapturerInit()) { + LOG_ERROR("Recreate screen capturer failed"); + return -1; + } + } + if (screen_capturer_) { LOG_INFO("Start screen capturer, show cursor: {}", show_cursor_); - screen_capturer_->Start(show_cursor_); + const int ret = screen_capturer_->Start(show_cursor_); + if (ret != 0) { + LOG_ERROR("Start screen capturer failed: {}", ret); + return ret; + } } return 0; @@ -650,14 +664,42 @@ int Render::StartMouseController() { LOG_INFO("Device controller factory is nullptr"); return -1; } + +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + if (!screen_capturer_) { + return 1; + } + + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (latest_display_info.empty() || + latest_display_info[0].handle == nullptr) { + return 1; + } + } + + if (screen_capturer_) { + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; + } + } +#endif + mouse_controller_ = (MouseController*)device_controller_factory_->Create( DeviceControllerFactory::Device::Mouse); + if (!mouse_controller_) { + LOG_ERROR("Create mouse controller failed"); + return -1; + } int mouse_controller_init_ret = mouse_controller_->Init(display_info_list_); if (0 != mouse_controller_init_ret) { LOG_INFO("Destroy mouse controller"); mouse_controller_->Destroy(); + delete mouse_controller_; mouse_controller_ = nullptr; + return mouse_controller_init_ret; } return 0; @@ -924,9 +966,24 @@ int Render::AudioDeviceDestroy() { } void Render::UpdateInteractions() { +#if defined(__linux__) && !defined(__APPLE__) + const bool is_wayland_session = IsWaylandSession(); + const bool stop_wayland_mouse_before_screen = + is_wayland_session && !start_screen_capturer_ && + screen_capturer_is_started_ && !start_mouse_controller_ && + mouse_controller_is_started_; + if (stop_wayland_mouse_before_screen) { + LOG_INFO("Stopping Wayland mouse controller before screen capturer to " + "cleanly release the shared portal session"); + StopMouseController(); + mouse_controller_is_started_ = false; + } +#endif + if (start_screen_capturer_ && !screen_capturer_is_started_) { - StartScreenCapturer(); - screen_capturer_is_started_ = true; + if (0 == StartScreenCapturer()) { + screen_capturer_is_started_ = true; + } } else if (!start_screen_capturer_ && screen_capturer_is_started_) { StopScreenCapturer(); screen_capturer_is_started_ = false; @@ -941,13 +998,24 @@ void Render::UpdateInteractions() { } if (start_mouse_controller_ && !mouse_controller_is_started_) { - StartMouseController(); - mouse_controller_is_started_ = true; + if (0 == StartMouseController()) { + mouse_controller_is_started_ = true; + } } else if (!start_mouse_controller_ && mouse_controller_is_started_) { StopMouseController(); mouse_controller_is_started_ = false; } +#if defined(__linux__) && !defined(__APPLE__) + if (screen_capturer_is_started_ && screen_capturer_ && mouse_controller_) { + const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); + if (!latest_display_info.empty()) { + display_info_list_ = latest_display_info; + mouse_controller_->UpdateDisplayInfoList(display_info_list_); + } + } +#endif + if (start_keyboard_capturer_ && focus_on_stream_window_) { if (!keyboard_capturer_is_started_) { StartKeyboardCapturer(); @@ -1439,10 +1507,8 @@ int Render::DrawStreamWindow() { auto props = it.second; if (props->tab_selected_) { SDL_FRect render_rect_f = { - static_cast(props->stream_render_rect_.x), - static_cast(props->stream_render_rect_.y), - static_cast(props->stream_render_rect_.w), - static_cast(props->stream_render_rect_.h)}; + props->stream_render_rect_f_.x, props->stream_render_rect_f_.y, + props->stream_render_rect_f_.w, props->stream_render_rect_f_.h}; SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL, &render_rect_f); } @@ -1850,6 +1916,12 @@ void Render::HandleServerWindow() { void Render::Cleanup() { Clipboard::StopMonitoring(); + if (mouse_controller_) { + mouse_controller_->Destroy(); + delete mouse_controller_; + mouse_controller_ = nullptr; + } + if (screen_capturer_) { screen_capturer_->Destroy(); delete screen_capturer_; @@ -1862,12 +1934,6 @@ void Render::Cleanup() { speaker_capturer_ = nullptr; } - if (mouse_controller_) { - mouse_controller_->Destroy(); - delete mouse_controller_; - mouse_controller_ = nullptr; - } - if (keyboard_capturer_) { delete keyboard_capturer_; keyboard_capturer_ = nullptr; @@ -1949,9 +2015,9 @@ void Render::CleanupPeers() { LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_); LeaveConnection(peer_, client_id_); is_client_mode_ = false; + StopMouseController(); StopScreenCapturer(); StopSpeakerCapturer(); - StopMouseController(); StopKeyboardCapturer(); LOG_INFO("Destroy peer [{}]", client_id_); DestroyPeer(&peer_); @@ -2229,26 +2295,36 @@ void Render::UpdateRenderRect() { float render_area_height = props->render_window_height_; props->stream_render_rect_last_ = props->stream_render_rect_; + + SDL_FRect rect_f{props->render_window_x_, props->render_window_y_, + render_area_width, render_area_height}; if (render_area_width < render_area_height * video_ratio) { - props->stream_render_rect_ = { - (int)props->render_window_x_, - (int)(abs(render_area_height - - render_area_width * video_ratio_reverse) / - 2 + - (int)props->render_window_y_), - (int)render_area_width, - (int)(render_area_width * video_ratio_reverse)}; + rect_f.x = props->render_window_x_; + rect_f.y = std::abs(render_area_height - + render_area_width * video_ratio_reverse) / + 2.0f + + props->render_window_y_; + rect_f.w = render_area_width; + rect_f.h = render_area_width * video_ratio_reverse; } else if (render_area_width > render_area_height * video_ratio) { - props->stream_render_rect_ = { - (int)abs(render_area_width - render_area_height * video_ratio) / 2 + - (int)props->render_window_x_, - (int)props->render_window_y_, (int)(render_area_height * video_ratio), - (int)render_area_height}; + rect_f.x = + std::abs(render_area_width - render_area_height * video_ratio) / 2.0f + + props->render_window_x_; + rect_f.y = props->render_window_y_; + rect_f.w = render_area_height * video_ratio; + rect_f.h = render_area_height; } else { - props->stream_render_rect_ = { - (int)props->render_window_x_, (int)props->render_window_y_, - (int)render_area_width, (int)render_area_height}; + rect_f.x = props->render_window_x_; + rect_f.y = props->render_window_y_; + rect_f.w = render_area_width; + rect_f.h = render_area_height; } + + props->stream_render_rect_f_ = rect_f; + props->stream_render_rect_ = {static_cast(std::lround(rect_f.x)), + static_cast(std::lround(rect_f.y)), + static_cast(std::lround(rect_f.w)), + static_cast(std::lround(rect_f.h))}; } } @@ -2389,12 +2465,23 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: - case SDL_EVENT_MOUSE_WHEEL: + case SDL_EVENT_MOUSE_WHEEL: { + Uint32 mouse_window_id = 0; + if (event.type == SDL_EVENT_MOUSE_MOTION) { + mouse_window_id = event.motion.windowID; + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + mouse_window_id = event.button.windowID; + } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { + mouse_window_id = event.wheel.windowID; + } + if (focus_on_stream_window_ && stream_window_ && - SDL_GetWindowID(stream_window_) == event.motion.windowID) { + SDL_GetWindowID(stream_window_) == mouse_window_id) { ProcessMouseEvent(event); } break; + } default: if (event.type == STREAM_REFRESH_EVENT) { diff --git a/src/gui/render.h b/src/gui/render.h index d3701e8..d36dc1f 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -160,6 +160,7 @@ class Render { SDL_Texture* stream_texture_ = nullptr; uint8_t* argb_buffer_ = nullptr; int argb_buffer_size_ = 0; + SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f}; SDL_Rect stream_render_rect_; SDL_Rect stream_render_rect_last_; ImVec2 control_window_pos_; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index ff4e200..f0f10e9 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -109,10 +110,67 @@ int Render::SendKeyCommand(int key_code, bool is_down) { int Render::ProcessMouseEvent(const SDL_Event& event) { controlled_remote_id_ = ""; - int video_width, video_height = 0; - int render_width, render_height = 0; - float ratio_x, ratio_y = 0; RemoteAction remote_action; + float cursor_x = last_mouse_event.motion.x; + float cursor_y = last_mouse_event.motion.y; + + auto normalize_cursor_to_window_space = [&](float* x, float* y) { + if (!x || !y || !stream_window_) { + return; + } + + int window_width = 0; + int window_height = 0; + int pixel_width = 0; + int pixel_height = 0; + SDL_GetWindowSize(stream_window_, &window_width, &window_height); + SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height); + + if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 || + pixel_height <= 0) { + return; + } + + if ((window_width != pixel_width || window_height != pixel_height) && + (*x > static_cast(window_width) + 1.0f || + *y > static_cast(window_height) + 1.0f)) { + const float scale_x = + static_cast(window_width) / static_cast(pixel_width); + const float scale_y = + static_cast(window_height) / static_cast(pixel_height); + *x *= scale_x; + *y *= scale_y; + + static bool logged_pixel_to_window_conversion = false; + if (!logged_pixel_to_window_conversion) { + LOG_INFO( + "Mouse coordinate space converted from pixels to window units: " + "window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})", + window_width, window_height, pixel_width, pixel_height, scale_x, + scale_y); + logged_pixel_to_window_conversion = true; + } + } + }; + + if (event.type == SDL_EVENT_MOUSE_MOTION) { + cursor_x = event.motion.x; + cursor_y = event.motion.y; + normalize_cursor_to_window_space(&cursor_x, &cursor_y); + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + cursor_x = event.button.x; + cursor_y = event.button.y; + normalize_cursor_to_window_space(&cursor_x, &cursor_y); + } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { + cursor_x = last_mouse_event.motion.x; + cursor_y = last_mouse_event.motion.y; + } + + const bool is_pointer_position_event = + (event.type == SDL_EVENT_MOUSE_MOTION || + event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + event.type == SDL_EVENT_MOUSE_BUTTON_UP); // std::shared_lock lock(client_properties_mutex_); for (auto& it : client_properties_) { @@ -121,23 +179,24 @@ int Render::ProcessMouseEvent(const SDL_Event& event) { continue; } - if (event.button.x >= props->stream_render_rect_.x && - event.button.x <= - props->stream_render_rect_.x + props->stream_render_rect_.w && - event.button.y >= props->stream_render_rect_.y && - event.button.y <= - props->stream_render_rect_.y + props->stream_render_rect_.h) { - controlled_remote_id_ = it.first; - render_width = props->stream_render_rect_.w; - render_height = props->stream_render_rect_.h; - last_mouse_event.button.x = event.button.x; - last_mouse_event.button.y = event.button.y; + const SDL_FRect render_rect = props->stream_render_rect_f_; + if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) { + continue; + } - remote_action.m.x = - (float)(event.button.x - props->stream_render_rect_.x) / render_width; - remote_action.m.y = - (float)(event.button.y - props->stream_render_rect_.y) / - render_height; + if (is_pointer_position_event && cursor_x >= render_rect.x && + cursor_x <= render_rect.x + render_rect.w && cursor_y >= render_rect.y && + cursor_y <= render_rect.y + render_rect.h) { + controlled_remote_id_ = it.first; + last_mouse_event.motion.x = cursor_x; + last_mouse_event.motion.y = cursor_y; + last_mouse_event.button.x = cursor_x; + last_mouse_event.button.y = cursor_y; + + remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w; + remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h; + remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); + remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) { remote_action.type = ControlType::mouse; @@ -171,12 +230,10 @@ int Render::ProcessMouseEvent(const SDL_Event& event) { props->data_label_.c_str()); } } else if (SDL_EVENT_MOUSE_WHEEL == event.type && - last_mouse_event.button.x >= props->stream_render_rect_.x && - last_mouse_event.button.x <= props->stream_render_rect_.x + - props->stream_render_rect_.w && - last_mouse_event.button.y >= props->stream_render_rect_.y && - last_mouse_event.button.y <= props->stream_render_rect_.y + - props->stream_render_rect_.h) { + last_mouse_event.button.x >= render_rect.x && + last_mouse_event.button.x <= render_rect.x + render_rect.w && + last_mouse_event.button.y >= render_rect.y && + last_mouse_event.button.y <= render_rect.y + render_rect.h) { float scroll_x = event.wheel.x; float scroll_y = event.wheel.y; if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { @@ -203,14 +260,12 @@ int Render::ProcessMouseEvent(const SDL_Event& event) { remote_action.m.s = roundUp(scroll_x); } - render_width = props->stream_render_rect_.w; - render_height = props->stream_render_rect_.h; - remote_action.m.x = - (float)(last_mouse_event.button.x - props->stream_render_rect_.x) / - render_width; - remote_action.m.y = - (float)(last_mouse_event.button.y - props->stream_render_rect_.y) / - render_height; + remote_action.m.x = (last_mouse_event.button.x - render_rect.x) / + (std::max)(render_rect.w, 1.0f); + remote_action.m.y = (last_mouse_event.button.y - render_rect.y) / + (std::max)(render_rect.h, 1.0f); + remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f); + remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f); if (props->control_bar_hovered_) { continue; @@ -783,6 +838,9 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, 0, (int)render->title_bar_height_, (int)render->stream_window_width_, (int)(render->stream_window_height_ - render->title_bar_height_)}; + props->stream_render_rect_f_ = { + 0.0f, render->title_bar_height_, render->stream_window_width_, + render->stream_window_height_ - render->title_bar_height_}; render->start_keyboard_capturer_ = true; break; } @@ -910,7 +968,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, })) { render->need_to_destroy_server_window_ = true; render->is_server_mode_ = false; +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + // Keep Wayland capture session warm to avoid black screen on + // subsequent reconnects. + render->start_screen_capturer_ = true; + LOG_INFO("Keeping Wayland screen capturer running after " + "disconnect to preserve reconnect stability"); + } else { + render->start_screen_capturer_ = false; + } +#else render->start_screen_capturer_ = false; +#endif render->start_speaker_capturer_ = false; render->start_mouse_controller_ = false; render->start_keyboard_capturer_ = false; @@ -1074,4 +1144,4 @@ void Render::OnNetStatusReport(const char* client_id, size_t client_id_size, props->net_traffic_stats_ = *net_traffic_stats; } } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/screen_capturer/linux/screen_capturer_linux.cpp b/src/screen_capturer/linux/screen_capturer_linux.cpp index 5fca5a1..87e51dc 100644 --- a/src/screen_capturer/linux/screen_capturer_linux.cpp +++ b/src/screen_capturer/linux/screen_capturer_linux.cpp @@ -6,6 +6,7 @@ #include #include +#include "platform.h" #include "rd_log.h" #if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM #include "screen_capturer_drm.h" @@ -19,16 +20,6 @@ namespace crossdesk { namespace { -bool IsWaylandSession() { - const char* session_type = getenv("XDG_SESSION_TYPE"); - if (session_type && strcmp(session_type, "wayland") == 0) { - return true; - } - - const char* wayland_display = getenv("WAYLAND_DISPLAY"); - return wayland_display && wayland_display[0] != '\0'; -} - #if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM constexpr bool kDrmBuildEnabled = true; #else @@ -162,6 +153,16 @@ 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); + } + } +#endif + const int ret = impl_->Start(show_cursor); if (ret == 0) { return 0; @@ -211,7 +212,9 @@ int ScreenCapturerLinux::Stop() { if (!impl_) { return 0; } - return impl_->Stop(); + const int ret = impl_->Stop(); + UpdateAliasesFromBackend(impl_.get()); + return ret; } int ScreenCapturerLinux::Pause(int monitor_index) { @@ -243,16 +246,19 @@ int ScreenCapturerLinux::ResetToInitialMonitor() { } std::vector ScreenCapturerLinux::GetDisplayInfoList() { - { - std::lock_guard lock(alias_mutex_); - if (!canonical_displays_.empty()) { - return canonical_displays_; - } - } - if (!impl_) { return std::vector(); } + + // Wayland backend may update display geometry/stream handle asynchronously + // after Start(). Refresh aliases every time to keep canonical displays fresh. + UpdateAliasesFromBackend(impl_.get()); + + std::lock_guard lock(alias_mutex_); + if (!canonical_displays_.empty()) { + return canonical_displays_; + } + return impl_->GetDisplayInfoList(); } @@ -314,6 +320,29 @@ int ScreenCapturerLinux::InitWayland() { #endif } +int ScreenCapturerLinux::RefreshWaylandBackend() { +#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER + auto backend = std::make_unique(); + const int ret = backend->Init(fps_, callback_); + if (ret != 0) { + backend->Destroy(); + return ret; + } + + if (impl_) { + impl_->Destroy(); + } + + UpdateAliasesFromBackend(backend.get()); + impl_ = std::move(backend); + backend_ = BackendType::kWayland; + LOG_INFO("Linux screen capturer Wayland backend refreshed before start"); + return 0; +#else + return -1; +#endif +} + bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) { #if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM auto drm_backend = std::make_unique(); @@ -443,6 +472,8 @@ void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) { 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; diff --git a/src/screen_capturer/linux/screen_capturer_linux.h b/src/screen_capturer/linux/screen_capturer_linux.h index b3d76e9..081fea3 100644 --- a/src/screen_capturer/linux/screen_capturer_linux.h +++ b/src/screen_capturer/linux/screen_capturer_linux.h @@ -43,6 +43,7 @@ class ScreenCapturerLinux : public ScreenCapturer { int InitX11(); int InitDrm(); int InitWayland(); + int RefreshWaylandBackend(); bool TryFallbackToDrm(bool show_cursor); bool TryFallbackToX11(bool show_cursor); bool TryFallbackToWayland(bool show_cursor); diff --git a/src/screen_capturer/linux/screen_capturer_wayland.cpp b/src/screen_capturer/linux/screen_capturer_wayland.cpp index fc3fb80..e43b08a 100644 --- a/src/screen_capturer/linux/screen_capturer_wayland.cpp +++ b/src/screen_capturer/linux/screen_capturer_wayland.cpp @@ -12,22 +12,27 @@ #include #include +#include "platform.h" #include "rd_log.h" +#include "wayland_portal_shared.h" namespace crossdesk { namespace { -bool IsWaylandSession() { - const char* session_type = getenv("XDG_SESSION_TYPE"); - if (session_type && strcmp(session_type, "wayland") == 0) { - return true; - } - - const char* wayland_display = getenv("WAYLAND_DISPLAY"); - return wayland_display && wayland_display[0] != '\0'; +int64_t NowMs() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); } +struct PipeWireRecoveryConfig { + ScreenCapturerWayland::PipeWireConnectMode mode; + bool relaxed_connect = false; +}; + +constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200); + } // namespace ScreenCapturerWayland::ScreenCapturerWayland() {} @@ -54,6 +59,8 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) { fps_ = fps; callback_ = cb; + pointer_granted_ = false; + shared_session_registered_ = false; display_info_list_.clear(); display_info_list_.push_back( DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight)); @@ -62,6 +69,8 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) { frame_width_ = kFallbackWidth; frame_height_ = kFallbackHeight; frame_stride_ = kFallbackWidth * 4; + logical_width_ = kFallbackWidth; + logical_height_ = kFallbackHeight; y_plane_.resize(kFallbackWidth * kFallbackHeight); uv_plane_.resize((kFallbackWidth / 2) * (kFallbackHeight / 2) * 2); @@ -84,6 +93,13 @@ int ScreenCapturerWayland::Start(bool show_cursor) { show_cursor_ = show_cursor; paused_ = false; + pipewire_node_id_ = 0; + UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth, + logical_height_ > 0 ? logical_height_ + : kFallbackHeight); + pipewire_format_ready_.store(false); + pipewire_stream_start_ms_.store(0); + pipewire_last_frame_ms_.store(0); running_ = true; thread_ = std::thread([this]() { Run(); }); return 0; @@ -94,6 +110,10 @@ int ScreenCapturerWayland::Stop() { if (thread_.joinable()) { thread_.join(); } + pipewire_node_id_ = 0; + UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth, + logical_height_ > 0 ? logical_height_ + : kFallbackHeight); return 0; } @@ -127,23 +147,96 @@ std::vector ScreenCapturerWayland::GetDisplayInfoList() { } void ScreenCapturerWayland::Run() { - if (!ConnectSessionBus() || !CreatePortalSession() || !SelectPortalSource() || - !StartPortalSession() || !OpenPipeWireRemote() || - !SetupPipeWireStream()) { + static constexpr PipeWireRecoveryConfig kRecoveryConfigs[] = { + {PipeWireConnectMode::kTargetObject, false}, + {PipeWireConnectMode::kAny, true}, + {PipeWireConnectMode::kNodeId, false}, + {PipeWireConnectMode::kNodeId, true}, + }; + + int recovery_index = 0; + auto setup_pipewire = [this, &recovery_index]() -> bool { + const auto& config = kRecoveryConfigs[recovery_index]; + return OpenPipeWireRemote() && + SetupPipeWireStream(config.relaxed_connect, config.mode); + }; + auto setup_pipeline = [this, &setup_pipewire]() -> bool { + return ConnectSessionBus() && CreatePortalSession() && + SelectPortalDevices() && SelectPortalSource() && + StartPortalSession() && setup_pipewire(); + }; + + if (!setup_pipeline()) { running_ = false; CleanupPipeWire(); ClosePortalSession(); CleanupDbus(); return; } - while (running_) { + if (!paused_) { + const int64_t now = NowMs(); + const int64_t stream_start = pipewire_stream_start_ms_.load(); + const int64_t last_frame = pipewire_last_frame_ms_.load(); + const bool format_ready = pipewire_format_ready_.load(); + + const bool format_timeout = + stream_start > 0 && !format_ready && (now - stream_start) > 1200; + const bool first_frame_timeout = + stream_start > 0 && format_ready && last_frame == 0 && + (now - stream_start) > 4000; + const bool frame_stall = last_frame > 0 && (now - last_frame) > 5000; + + if (format_timeout || first_frame_timeout || frame_stall) { + if (recovery_index + 1 >= + static_cast(sizeof(kRecoveryConfigs) / + sizeof(kRecoveryConfigs[0]))) { + LOG_ERROR( + "Wayland capture stalled and recovery limit reached, " + "format_ready={}, stream_start={}, last_frame={}, attempts={}", + format_ready, stream_start, last_frame, recovery_index); + running_ = false; + break; + } + + ++recovery_index; + const char* reason = format_timeout + ? "format-timeout" + : (first_frame_timeout ? "first-frame-timeout" + : "frame-stall"); + const auto& config = kRecoveryConfigs[recovery_index]; + LOG_WARN( + "Wayland capture stalled ({}) - retrying PipeWire only, " + "attempt {}/{}, mode={}, relaxed_connect={}", + reason, recovery_index, + static_cast(sizeof(kRecoveryConfigs) / + sizeof(kRecoveryConfigs[0])) - + 1, + config.mode == PipeWireConnectMode::kTargetObject + ? "target-object" + : (config.mode == PipeWireConnectMode::kNodeId ? "node-id" + : "any"), + config.relaxed_connect); + + CleanupPipeWire(); + if (!setup_pipewire()) { + LOG_ERROR("Wayland PipeWire-only recovery failed at attempt {}", + recovery_index); + running_ = false; + break; + } + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } CleanupPipeWire(); + if (!session_handle_.empty()) { + std::this_thread::sleep_for(kPipeWireCloseSettleDelay); + } ClosePortalSession(); CleanupDbus(); } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/screen_capturer/linux/screen_capturer_wayland.h b/src/screen_capturer/linux/screen_capturer_wayland.h index ce4616c..58f8f16 100644 --- a/src/screen_capturer/linux/screen_capturer_wayland.h +++ b/src/screen_capturer/linux/screen_capturer_wayland.h @@ -24,6 +24,9 @@ struct pw_thread_loop; namespace crossdesk { class ScreenCapturerWayland : public ScreenCapturer { + public: + enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny }; + public: ScreenCapturerWayland(); ~ScreenCapturerWayland(); @@ -46,10 +49,11 @@ class ScreenCapturerWayland : public ScreenCapturer { bool CheckPortalAvailability() const; bool ConnectSessionBus(); bool CreatePortalSession(); + bool SelectPortalDevices(); bool SelectPortalSource(); bool StartPortalSession(); bool OpenPipeWireRemote(); - bool SetupPipeWireStream(); + bool SetupPipeWireStream(bool relaxed_connect, PipeWireConnectMode mode); void Run(); void CleanupPipeWire(); @@ -66,6 +70,9 @@ class ScreenCapturerWayland : public ScreenCapturer { std::atomic running_{false}; std::atomic paused_{false}; std::atomic monitor_index_{0}; + std::atomic pipewire_format_ready_{false}; + std::atomic pipewire_stream_start_ms_{0}; + std::atomic pipewire_last_frame_ms_{0}; int initial_monitor_index_ = 0; std::atomic show_cursor_{true}; int fps_ = 60; @@ -85,10 +92,14 @@ class ScreenCapturerWayland : public ScreenCapturer { void* stream_listener_ = nullptr; bool pipewire_initialized_ = false; bool pipewire_thread_loop_started_ = false; + bool pointer_granted_ = false; + bool shared_session_registered_ = false; uint32_t spa_video_format_ = 0; int frame_width_ = 0; int frame_height_ = 0; int frame_stride_ = 0; + int logical_width_ = 0; + int logical_height_ = 0; std::vector y_plane_; std::vector uv_plane_; diff --git a/src/screen_capturer/linux/screen_capturer_wayland_build.h b/src/screen_capturer/linux/screen_capturer_wayland_build.h index 69bfb6a..613a9fd 100644 --- a/src/screen_capturer/linux/screen_capturer_wayland_build.h +++ b/src/screen_capturer/linux/screen_capturer_wayland_build.h @@ -16,11 +16,27 @@ #include #include #include +#include #include #include #include +#include #include +#if defined(__has_include) +#if __has_include() +#include +#endif +#endif + +#define CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS 1u +#define CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS 2u +#define CROSSDESK_SPA_PARAM_BUFFERS_SIZE 3u +#define CROSSDESK_SPA_PARAM_BUFFERS_STRIDE 4u + +#define CROSSDESK_SPA_PARAM_META_TYPE 1u +#define CROSSDESK_SPA_PARAM_META_SIZE 2u + #else #define CROSSDESK_WAYLAND_BUILD_ENABLED 0 diff --git a/src/screen_capturer/linux/screen_capturer_wayland_pipewire.cpp b/src/screen_capturer/linux/screen_capturer_wayland_pipewire.cpp index 2e31325..6fe69b6 100644 --- a/src/screen_capturer/linux/screen_capturer_wayland_pipewire.cpp +++ b/src/screen_capturer/linux/screen_capturer_wayland_pipewire.cpp @@ -4,6 +4,7 @@ #if CROSSDESK_WAYLAND_BUILD_ENABLED +#include #include #include #include @@ -23,14 +24,154 @@ const char* PipeWireFormatName(uint32_t spa_format) { return "BGRx"; case SPA_VIDEO_FORMAT_BGRA: return "BGRA"; +#ifdef SPA_VIDEO_FORMAT_RGBx + case SPA_VIDEO_FORMAT_RGBx: + return "RGBx"; +#endif +#ifdef SPA_VIDEO_FORMAT_RGBA + case SPA_VIDEO_FORMAT_RGBA: + return "RGBA"; +#endif default: return "unsupported"; } } +const char* PipeWireConnectModeName( + ScreenCapturerWayland::PipeWireConnectMode mode) { + switch (mode) { + case ScreenCapturerWayland::PipeWireConnectMode::kTargetObject: + return "target-object"; + case ScreenCapturerWayland::PipeWireConnectMode::kNodeId: + return "node-id"; + case ScreenCapturerWayland::PipeWireConnectMode::kAny: + return "any"; + default: + return "unknown"; + } +} + +int64_t NowMs() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +struct PipeWireTargetLookupState { + pw_thread_loop* loop = nullptr; + uint32_t target_node_id = 0; + int sync_seq = -1; + bool done = false; + bool found = false; + std::string object_serial; +}; + +std::string LookupPipeWireTargetObjectSerial(pw_core* core, + pw_thread_loop* loop, + uint32_t node_id) { + if (!core || !loop || node_id == 0) { + return ""; + } + + PipeWireTargetLookupState state; + state.loop = loop; + state.target_node_id = node_id; + + pw_registry* registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0); + if (!registry) { + return ""; + } + + spa_hook registry_listener{}; + spa_hook core_listener{}; + + pw_registry_events registry_events{}; + registry_events.version = PW_VERSION_REGISTRY_EVENTS; + registry_events.global = + [](void* userdata, uint32_t id, uint32_t permissions, const char* type, + uint32_t version, const spa_dict* props) { + (void)permissions; + (void)version; + auto* state = static_cast(userdata); + if (!state || !props || id != state->target_node_id || !type) { + return; + } + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) { + return; + } + + const char* object_serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL); + if (!object_serial || object_serial[0] == '\0') { + object_serial = spa_dict_lookup(props, "object.serial"); + } + if (!object_serial || object_serial[0] == '\0') { + return; + } + + state->object_serial = object_serial; + state->found = true; + }; + + pw_core_events core_events{}; + core_events.version = PW_VERSION_CORE_EVENTS; + core_events.done = [](void* userdata, uint32_t id, int seq) { + auto* state = static_cast(userdata); + if (!state || id != PW_ID_CORE || seq != state->sync_seq) { + return; + } + state->done = true; + pw_thread_loop_signal(state->loop, false); + }; + core_events.error = [](void* userdata, uint32_t id, int seq, int res, + const char* message) { + (void)id; + (void)seq; + (void)res; + auto* state = static_cast(userdata); + if (!state) { + return; + } + LOG_WARN("PipeWire registry lookup error: {}", + message ? message : "unknown"); + state->done = true; + pw_thread_loop_signal(state->loop, false); + }; + + pw_registry_add_listener(registry, ®istry_listener, ®istry_events, + &state); + pw_core_add_listener(core, &core_listener, &core_events, &state); + state.sync_seq = pw_core_sync(core, PW_ID_CORE, 0); + + while (!state.done) { + pw_thread_loop_wait(loop); + } + + spa_hook_remove(®istry_listener); + spa_hook_remove(&core_listener); + pw_proxy_destroy(reinterpret_cast(registry)); + return state.found ? state.object_serial : ""; +} + +int BytesPerPixel(uint32_t spa_format) { + switch (spa_format) { + case SPA_VIDEO_FORMAT_BGRx: + case SPA_VIDEO_FORMAT_BGRA: +#ifdef SPA_VIDEO_FORMAT_RGBx + case SPA_VIDEO_FORMAT_RGBx: +#endif +#ifdef SPA_VIDEO_FORMAT_RGBA + case SPA_VIDEO_FORMAT_RGBA: +#endif + return 4; + default: + return 0; + } +} + } // namespace -bool ScreenCapturerWayland::SetupPipeWireStream() { +bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect, + PipeWireConnectMode mode) { if (pipewire_fd_ < 0 || pipewire_node_id_ == 0) { return false; } @@ -73,10 +214,35 @@ bool ScreenCapturerWayland::SetupPipeWireStream() { } pipewire_fd_ = -1; - pw_stream_ = pw_stream_new( - pw_core_, "CrossDesk Wayland Capture", + pw_properties* stream_props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, - "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr)); + "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr); + if (!stream_props) { + LOG_ERROR("Failed to allocate PipeWire stream properties"); + pw_thread_loop_unlock(pw_thread_loop_); + CleanupPipeWire(); + return false; + } + + std::string target_object_serial; + if (mode == PipeWireConnectMode::kTargetObject) { + target_object_serial = + LookupPipeWireTargetObjectSerial(pw_core_, pw_thread_loop_, + pipewire_node_id_); + if (!target_object_serial.empty()) { + pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, + target_object_serial.c_str()); + LOG_INFO("PipeWire target object serial for node {} is {}", + pipewire_node_id_, target_object_serial); + } else { + LOG_WARN("PipeWire target object serial lookup failed for node {}, " + "falling back to direct target id in target-object mode", + pipewire_node_id_); + } + } + + pw_stream_ = pw_stream_new(pw_core_, "CrossDesk Wayland Capture", + stream_props); if (!pw_stream_) { LOG_ERROR("Failed to create PipeWire stream"); pw_thread_loop_unlock(pw_thread_loop_); @@ -108,6 +274,7 @@ bool ScreenCapturerWayland::SetupPipeWireStream() { LOG_INFO("PipeWire stream state: {} -> {}", pw_stream_state_as_string(old_state), pw_stream_state_as_string(state)); + }; events.param_changed = [](void* userdata, uint32_t id, const struct spa_pod* param) { @@ -127,18 +294,84 @@ bool ScreenCapturerWayland::SetupPipeWireStream() { self->frame_height_ = static_cast(info.size.height); self->frame_stride_ = static_cast(info.size.width) * 4; - if (self->spa_video_format_ != SPA_VIDEO_FORMAT_BGRx && - self->spa_video_format_ != SPA_VIDEO_FORMAT_BGRA) { + bool supported_format = + (self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRx) || + (self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRA); +#ifdef SPA_VIDEO_FORMAT_RGBx + supported_format = + supported_format || + (self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBx); +#endif +#ifdef SPA_VIDEO_FORMAT_RGBA + supported_format = + supported_format || + (self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBA); +#endif + if (!supported_format) { LOG_ERROR("Unsupported PipeWire pixel format: {}", PipeWireFormatName(self->spa_video_format_)); self->running_ = false; return; } - self->UpdateDisplayGeometry(self->frame_width_, self->frame_height_); - LOG_INFO("PipeWire video format: {}, {}x{}", + const int bytes_per_pixel = BytesPerPixel(self->spa_video_format_); + if (bytes_per_pixel <= 0 || self->frame_width_ <= 0 || + self->frame_height_ <= 0) { + LOG_ERROR("Invalid PipeWire frame layout: format={}, size={}x{}", + PipeWireFormatName(self->spa_video_format_), + self->frame_width_, self->frame_height_); + self->running_ = false; + return; + } + + self->frame_stride_ = self->frame_width_ * bytes_per_pixel; + + uint8_t buffer[1024]; + spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + const spa_pod* params[2]; + uint32_t param_count = 0; + + params[param_count++] = reinterpret_cast( + spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS, + SPA_POD_CHOICE_RANGE_Int(8, 4, 16), + CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS, SPA_POD_Int(1), + CROSSDESK_SPA_PARAM_BUFFERS_SIZE, + SPA_POD_CHOICE_RANGE_Int(self->frame_stride_ * + self->frame_height_, + self->frame_stride_ * + self->frame_height_, + self->frame_stride_ * + self->frame_height_), + CROSSDESK_SPA_PARAM_BUFFERS_STRIDE, + SPA_POD_CHOICE_RANGE_Int(self->frame_stride_, + self->frame_stride_, + self->frame_stride_))); + + params[param_count++] = reinterpret_cast( + spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + CROSSDESK_SPA_PARAM_META_TYPE, SPA_POD_Id(SPA_META_Header), + CROSSDESK_SPA_PARAM_META_SIZE, + SPA_POD_Int(sizeof(struct spa_meta_header)))); + + if (self->pw_stream_) { + pw_stream_update_params(self->pw_stream_, params, param_count); + } + self->pipewire_format_ready_.store(true); + + const int pointer_width = + self->logical_width_ > 0 ? self->logical_width_ : self->frame_width_; + const int pointer_height = self->logical_height_ > 0 + ? self->logical_height_ + : self->frame_height_; + self->UpdateDisplayGeometry(pointer_width, pointer_height); + LOG_INFO( + "PipeWire video format: {}, {}x{} stride={} (pointer space {}x{})", PipeWireFormatName(self->spa_video_format_), - self->frame_width_, self->frame_height_); + self->frame_width_, self->frame_height_, self->frame_stride_, + pointer_width, pointer_height); }; events.process = [](void* userdata) { auto* self = static_cast(userdata); @@ -150,29 +383,74 @@ bool ScreenCapturerWayland::SetupPipeWireStream() { }(); pw_stream_add_listener(pw_stream_, listener, &stream_events, this); + pipewire_format_ready_.store(false); + pipewire_stream_start_ms_.store(NowMs()); + pipewire_last_frame_ms_.store(0); - uint8_t buffer[1024]; + uint8_t buffer[4096]; spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); - const spa_pod* params[1]; - const spa_rectangle min_size{1, 1}; - const spa_rectangle max_size{8192, 8192}; - const spa_rectangle default_size{kFallbackWidth, kFallbackHeight}; - const spa_fraction any_rate{0, 1}; + const spa_pod* params[8]; + int param_count = 0; + const spa_rectangle fixed_size{ + static_cast(logical_width_ > 0 ? logical_width_ : kFallbackWidth), + static_cast(logical_height_ > 0 ? logical_height_ + : kFallbackHeight)}; + const spa_rectangle min_size{1u, 1u}; + const spa_rectangle max_size{16384u, 16384u}; - params[0] = reinterpret_cast(spa_pod_builder_add_object( - &builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, - SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_BGRx), - SPA_FORMAT_VIDEO_size, - SPA_POD_CHOICE_RANGE_Rectangle(&default_size, &min_size, &max_size), - SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&any_rate))); + if (!relaxed_connect) { + auto add_format_param = [&](uint32_t spa_format) { + if (param_count >= static_cast(sizeof(params) / sizeof(params[0]))) { + return; + } + params[param_count++] = + reinterpret_cast(spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(spa_format), + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle(&fixed_size, &min_size, + &max_size))); + }; + add_format_param(SPA_VIDEO_FORMAT_BGRx); + add_format_param(SPA_VIDEO_FORMAT_BGRA); +#ifdef SPA_VIDEO_FORMAT_RGBx + add_format_param(SPA_VIDEO_FORMAT_RGBx); +#endif +#ifdef SPA_VIDEO_FORMAT_RGBA + add_format_param(SPA_VIDEO_FORMAT_RGBA); +#endif + + if (param_count == 0) { + LOG_ERROR("No valid PipeWire format params were built"); + pw_thread_loop_unlock(pw_thread_loop_); + CleanupPipeWire(); + return false; + } + } else { + LOG_INFO("PipeWire stream using relaxed format negotiation"); + } + + uint32_t target_id = PW_ID_ANY; + if (mode == PipeWireConnectMode::kNodeId || + (mode == PipeWireConnectMode::kTargetObject && + target_object_serial.empty())) { + target_id = pipewire_node_id_; + } + LOG_INFO( + "PipeWire connecting stream: mode={}, node_id={}, target_id={}, " + "target_object_serial={}, relaxed_connect={}, param_count={}, " + "requested_size={}x{}", + PipeWireConnectModeName(mode), pipewire_node_id_, target_id, + target_object_serial.empty() ? "none" : target_object_serial.c_str(), + relaxed_connect, param_count, fixed_size.width, fixed_size.height); const int ret = pw_stream_connect( - pw_stream_, PW_DIRECTION_INPUT, pipewire_node_id_, + pw_stream_, PW_DIRECTION_INPUT, target_id, static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), - params, 1); + param_count > 0 ? params : nullptr, static_cast(param_count)); pw_thread_loop_unlock(pw_thread_loop_); if (ret < 0) { @@ -193,16 +471,21 @@ void ScreenCapturerWayland::CleanupPipeWire() { } if (pw_stream_) { + pw_stream_set_active(pw_stream_, false); pw_stream_disconnect(pw_stream_); - pw_stream_destroy(pw_stream_); - pw_stream_ = nullptr; } if (stream_listener_) { + spa_hook_remove(static_cast(stream_listener_)); delete static_cast(stream_listener_); stream_listener_ = nullptr; } + if (pw_stream_) { + pw_stream_destroy(pw_stream_); + pw_stream_ = nullptr; + } + if (pw_core_) { pw_core_disconnect(pw_core_); pw_core_ = nullptr; @@ -231,6 +514,10 @@ void ScreenCapturerWayland::CleanupPipeWire() { pipewire_fd_ = -1; } + pipewire_format_ready_.store(false); + pipewire_stream_start_ms_.store(0); + pipewire_last_frame_ms_.store(0); + if (pipewire_initialized_) { pw_deinit(); pipewire_initialized_ = false; @@ -309,6 +596,7 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() { callback_(nv12.data(), static_cast(nv12.size()), even_width, even_height, display_name_.c_str()); } + pipewire_last_frame_ms_.store(NowMs()); requeue(); } @@ -318,15 +606,17 @@ void ScreenCapturerWayland::UpdateDisplayGeometry(int width, int height) { return; } - frame_width_ = width; - frame_height_ = height; + void* stream_handle = + reinterpret_cast(static_cast(pipewire_node_id_)); if (display_info_list_.empty()) { - display_info_list_.push_back(DisplayInfo(display_name_, 0, 0, width, height)); + display_info_list_.push_back( + DisplayInfo(stream_handle, display_name_, true, 0, 0, width, height)); return; } auto& display = display_info_list_[0]; + display.handle = stream_handle; display.left = 0; display.top = 0; display.right = width; diff --git a/src/screen_capturer/linux/screen_capturer_wayland_portal.cpp b/src/screen_capturer/linux/screen_capturer_wayland_portal.cpp index 720601a..d958697 100644 --- a/src/screen_capturer/linux/screen_capturer_wayland_portal.cpp +++ b/src/screen_capturer/linux/screen_capturer_wayland_portal.cpp @@ -1,6 +1,7 @@ #include "screen_capturer_wayland.h" #include "screen_capturer_wayland_build.h" +#include "wayland_portal_shared.h" #if CROSSDESK_WAYLAND_BUILD_ENABLED @@ -18,6 +19,8 @@ namespace { constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop"; constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop"; +constexpr const char* kPortalRemoteDesktopInterface = + "org.freedesktop.portal.RemoteDesktop"; constexpr const char* kPortalScreenCastInterface = "org.freedesktop.portal.ScreenCast"; constexpr const char* kPortalRequestInterface = @@ -32,6 +35,7 @@ constexpr const char* kPortalSessionPathPrefix = constexpr uint32_t kScreenCastSourceMonitor = 1u; constexpr uint32_t kCursorModeHidden = 1u; constexpr uint32_t kCursorModeEmbedded = 2u; +constexpr uint32_t kRemoteDesktopDevicePointer = 2u; std::string MakeToken(const char* prefix) { const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); @@ -279,19 +283,21 @@ bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code, } bool SendPortalRequestAndHandleResponse( - DBusConnection* connection, const char* method_name, + DBusConnection* connection, const char* interface_name, + const char* method_name, const char* action_name, const std::function& append_message_args, const std::atomic& running, const std::function& handle_results, std::string* request_path_out = nullptr) { - if (!connection || !method_name || method_name[0] == '\0') { + if (!connection || !interface_name || interface_name[0] == '\0' || + !method_name || method_name[0] == '\0') { return false; } DBusMessage* message = dbus_message_new_method_call(kPortalBusName, kPortalObjectPath, - kPortalScreenCastInterface, method_name); + interface_name, method_name); if (!message) { LOG_ERROR("Failed to allocate {} message", method_name); return false; @@ -399,7 +405,8 @@ bool ScreenCapturerWayland::CreatePortalSession() { const std::string session_handle_token = MakeToken("crossdesk_session"); std::string request_path; const bool ok = SendPortalRequestAndHandleResponse( - dbus_connection_, "CreateSession", "CreateSession", + dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession", + "CreateSession", [&](DBusMessage* message) { DBusMessageIter iter; DBusMessageIter options; @@ -478,7 +485,8 @@ bool ScreenCapturerWayland::SelectPortalSource() { const char* session_handle = session_handle_.c_str(); return SendPortalRequestAndHandleResponse( - dbus_connection_, "SelectSources", "SelectSources", + dbus_connection_, kPortalScreenCastInterface, "SelectSources", + "SelectSources", [&](DBusMessage* message) { DBusMessageIter iter; DBusMessageIter options; @@ -507,6 +515,39 @@ bool ScreenCapturerWayland::SelectPortalSource() { }); } +bool ScreenCapturerWayland::SelectPortalDevices() { + if (!dbus_connection_ || session_handle_.empty()) { + return false; + } + + const char* session_handle = session_handle_.c_str(); + return SendPortalRequestAndHandleResponse( + dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices", + "SelectDevices", + [&](DBusMessage* message) { + DBusMessageIter iter; + DBusMessageIter options; + dbus_message_iter_init_append(message, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, + &session_handle); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", + &options); + AppendDictEntryUint32(&options, "types", kRemoteDesktopDevicePointer); + AppendDictEntryString(&options, "handle_token", + MakeToken("crossdesk_req")); + dbus_message_iter_close_container(&iter, &options); + return true; + }, + running_, [](uint32_t response_code, DBusMessageIter*) { + if (response_code != 0) { + LOG_ERROR("SelectDevices was denied or malformed, response={}", + response_code); + return false; + } + return true; + }); +} + bool ScreenCapturerWayland::StartPortalSession() { if (!dbus_connection_ || session_handle_.empty()) { return false; @@ -514,8 +555,9 @@ bool ScreenCapturerWayland::StartPortalSession() { const char* session_handle = session_handle_.c_str(); const char* parent_window = ""; + pointer_granted_ = false; const bool ok = SendPortalRequestAndHandleResponse( - dbus_connection_, "Start", "Start", + dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start", [&](DBusMessage* message) { DBusMessageIter iter; DBusMessageIter options; @@ -536,6 +578,7 @@ bool ScreenCapturerWayland::StartPortalSession() { return false; } + uint32_t granted_devices = 0; DBusMessageIter dict; dbus_message_iter_recurse(results, &dict); while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) { @@ -546,55 +589,91 @@ bool ScreenCapturerWayland::StartPortalSession() { const char* key = nullptr; dbus_message_iter_get_basic(&entry, &key); if (key && dbus_message_iter_next(&entry) && - dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT && - strcmp(key, "streams") == 0) { + dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) { DBusMessageIter variant; - DBusMessageIter streams; dbus_message_iter_recurse(&entry, &variant); - dbus_message_iter_recurse(&variant, &streams); - - if (dbus_message_iter_get_arg_type(&streams) == DBUS_TYPE_STRUCT) { - DBusMessageIter stream; - dbus_message_iter_recurse(&streams, &stream); - - if (dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_UINT32) { - dbus_message_iter_get_basic(&stream, &pipewire_node_id_); + if (strcmp(key, "devices") == 0) { + int granted_devices_int = 0; + if (ReadIntLike(&variant, &granted_devices_int) && + granted_devices_int >= 0) { + granted_devices = static_cast(granted_devices_int); } + } else if (strcmp(key, "streams") == 0) { + DBusMessageIter streams; + dbus_message_iter_recurse(&variant, &streams); - if (dbus_message_iter_next(&stream) && - dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_ARRAY) { - DBusMessageIter props; - dbus_message_iter_recurse(&stream, &props); - while (dbus_message_iter_get_arg_type(&props) != - DBUS_TYPE_INVALID) { - if (dbus_message_iter_get_arg_type(&props) == - DBUS_TYPE_DICT_ENTRY) { - DBusMessageIter prop_entry; - dbus_message_iter_recurse(&props, &prop_entry); + if (dbus_message_iter_get_arg_type(&streams) == DBUS_TYPE_STRUCT) { + DBusMessageIter stream; + dbus_message_iter_recurse(&streams, &stream); - const char* prop_key = nullptr; - dbus_message_iter_get_basic(&prop_entry, &prop_key); - if (prop_key && dbus_message_iter_next(&prop_entry) && - dbus_message_iter_get_arg_type(&prop_entry) == - DBUS_TYPE_VARIANT && - strcmp(prop_key, "size") == 0) { - DBusMessageIter prop_variant; - dbus_message_iter_recurse(&prop_entry, &prop_variant); - if (dbus_message_iter_get_arg_type(&prop_variant) == - DBUS_TYPE_STRUCT) { - DBusMessageIter size_iter; - int width = 0; - int height = 0; - dbus_message_iter_recurse(&prop_variant, &size_iter); - if (ReadIntLike(&size_iter, &width) && - dbus_message_iter_next(&size_iter) && - ReadIntLike(&size_iter, &height)) { - UpdateDisplayGeometry(width, height); + if (dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&stream, &pipewire_node_id_); + } + + if (dbus_message_iter_next(&stream) && + dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_ARRAY) { + DBusMessageIter props; + int stream_width = 0; + int stream_height = 0; + int logical_width = 0; + int logical_height = 0; + dbus_message_iter_recurse(&stream, &props); + while (dbus_message_iter_get_arg_type(&props) != + DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&props) == + DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter prop_entry; + dbus_message_iter_recurse(&props, &prop_entry); + + const char* prop_key = nullptr; + dbus_message_iter_get_basic(&prop_entry, &prop_key); + if (prop_key && dbus_message_iter_next(&prop_entry) && + dbus_message_iter_get_arg_type(&prop_entry) == + DBUS_TYPE_VARIANT) { + DBusMessageIter prop_variant; + dbus_message_iter_recurse(&prop_entry, &prop_variant); + if (dbus_message_iter_get_arg_type(&prop_variant) == + DBUS_TYPE_STRUCT) { + DBusMessageIter size_iter; + int width = 0; + int height = 0; + dbus_message_iter_recurse(&prop_variant, &size_iter); + if (ReadIntLike(&size_iter, &width) && + dbus_message_iter_next(&size_iter) && + ReadIntLike(&size_iter, &height)) { + if (strcmp(prop_key, "logical_size") == 0) { + logical_width = width; + logical_height = height; + } else if (strcmp(prop_key, "size") == 0) { + stream_width = width; + stream_height = height; + } + } } } } + dbus_message_iter_next(&props); + } + + const int picked_width = + logical_width > 0 ? logical_width : stream_width; + const int picked_height = + logical_height > 0 ? logical_height : stream_height; + LOG_INFO( + "Wayland portal stream geometry: stream_size={}x{}, " + "logical_size={}x{}, pointer_space={}x{}", + stream_width, stream_height, logical_width, + logical_height, picked_width, picked_height); + + if (logical_width > 0 && logical_height > 0) { + logical_width_ = logical_width; + logical_height_ = logical_height; + UpdateDisplayGeometry(logical_width_, logical_height_); + } else if (stream_width > 0 && stream_height > 0) { + logical_width_ = stream_width; + logical_height_ = stream_height; + UpdateDisplayGeometry(logical_width_, logical_height_); } - dbus_message_iter_next(&props); } } } @@ -603,6 +682,8 @@ bool ScreenCapturerWayland::StartPortalSession() { dbus_message_iter_next(&dict); } + pointer_granted_ = + (granted_devices & kRemoteDesktopDevicePointer) != 0; return true; }); if (!ok) { @@ -613,6 +694,18 @@ bool ScreenCapturerWayland::StartPortalSession() { LOG_ERROR("Start response did not include a PipeWire node id"); return false; } + if (!pointer_granted_) { + LOG_ERROR("Start response did not grant pointer control"); + return false; + } + + shared_session_registered_ = PublishSharedWaylandPortalSession( + SharedWaylandPortalSessionInfo{ + dbus_connection_, session_handle_, pipewire_node_id_, logical_width_, + logical_height_, pointer_granted_}); + if (!shared_session_registered_) { + LOG_WARN("Failed to publish shared Wayland portal session"); + } LOG_INFO("Wayland screencast ready, node_id={}", pipewire_node_id_); return true; @@ -683,37 +776,39 @@ void ScreenCapturerWayland::CleanupDbus() { return; } + if (shared_session_registered_) { + return; + } + dbus_connection_close(dbus_connection_); dbus_connection_unref(dbus_connection_); dbus_connection_ = nullptr; } void ScreenCapturerWayland::ClosePortalSession() { - if (!dbus_connection_ || session_handle_.empty()) { - return; - } - - DBusMessage* message = dbus_message_new_method_call( - kPortalBusName, session_handle_.c_str(), kPortalSessionInterface, - "Close"); - if (message) { - DBusError error; - dbus_error_init(&error); - DBusMessage* reply = - dbus_connection_send_with_reply_and_block(dbus_connection_, message, - 1000, &error); - if (!reply && dbus_error_is_set(&error)) { - LogDbusError("Session.Close", &error); - dbus_error_free(&error); + if (shared_session_registered_) { + DBusConnection* close_connection = nullptr; + std::string close_session_handle; + ReleaseSharedWaylandPortalSession(&close_connection, &close_session_handle); + shared_session_registered_ = false; + if (close_connection) { + CloseWaylandPortalSessionAndConnection(close_connection, + close_session_handle, + "Session.Close"); } - if (reply) { - dbus_message_unref(reply); - } - dbus_message_unref(message); + dbus_connection_ = nullptr; + } else if (dbus_connection_ && !session_handle_.empty()) { + CloseWaylandPortalSessionAndConnection(dbus_connection_, session_handle_, + "Session.Close"); + dbus_connection_ = nullptr; } session_handle_.clear(); pipewire_node_id_ = 0; + UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth, + logical_height_ > 0 ? logical_height_ + : kFallbackHeight); + pointer_granted_ = false; } } // namespace crossdesk