mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-09-01 22:51:55 +08:00
[refactor] reorganize platform and wire sources
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
#include "platform/autostart_backend.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace crossdesk::platform {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path AutostartPath(const std::string& app_name) {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home || !home[0]) return {};
|
||||
return std::filesystem::path(home) / ".config" / "autostart" /
|
||||
(app_name + ".desktop");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string GetAutostartExecutablePath() {
|
||||
char path[PATH_MAX] = {};
|
||||
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
return count > 0 ? std::string(path, static_cast<size_t>(count))
|
||||
: std::string();
|
||||
}
|
||||
|
||||
bool EnableAutostart(const std::string& app_name,
|
||||
const std::string& executable_path) {
|
||||
const std::filesystem::path path = AutostartPath(app_name);
|
||||
if (path.empty()) return false;
|
||||
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(path.parent_path(), error);
|
||||
if (error) return false;
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) return false;
|
||||
file << "[Desktop Entry]\n"
|
||||
<< "Type=Application\n"
|
||||
<< "Exec=" << executable_path << "\n"
|
||||
<< "Hidden=false\n"
|
||||
<< "NoDisplay=false\n"
|
||||
<< "X-GNOME-Autostart-enabled=true\n"
|
||||
<< "Terminal=false\n"
|
||||
<< "StartupNotify=false\n"
|
||||
<< "Name=" << app_name << "\n";
|
||||
file.close();
|
||||
return file.good();
|
||||
}
|
||||
|
||||
bool DisableAutostart(const std::string& app_name) {
|
||||
const std::filesystem::path path = AutostartPath(app_name);
|
||||
if (path.empty()) return false;
|
||||
std::error_code error;
|
||||
return std::filesystem::remove(path, error) && !error;
|
||||
}
|
||||
|
||||
bool IsAutostartEnabled(const std::string& app_name) {
|
||||
const std::filesystem::path path = AutostartPath(app_name);
|
||||
return !path.empty() && std::filesystem::is_regular_file(path);
|
||||
}
|
||||
|
||||
} // namespace crossdesk::platform
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "clipboard.h"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "platform/clipboard_backend.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string Clipboard::GetText() {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (!display) {
|
||||
LOG_ERROR("Clipboard::GetText: failed to open X display");
|
||||
return {};
|
||||
}
|
||||
|
||||
Window owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||
if (owner == None) {
|
||||
owner =
|
||||
XGetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False));
|
||||
if (owner == None) {
|
||||
XCloseDisplay(display);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Atom selection = XA_PRIMARY;
|
||||
Atom target = XInternAtom(display, "UTF8_STRING", False);
|
||||
if (target == None) target = XA_STRING;
|
||||
const Window window = XCreateSimpleWindow(
|
||||
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
|
||||
XSelectInput(display, window, PropertyChangeMask);
|
||||
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||
CurrentTime);
|
||||
|
||||
std::string result;
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
XEvent event = {};
|
||||
XNextEvent(display, &event);
|
||||
if (event.type != SelectionNotify) continue;
|
||||
if (event.xselection.property == None) {
|
||||
if (selection == XA_PRIMARY) {
|
||||
selection = XInternAtom(display, "CLIPBOARD", False);
|
||||
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||
CurrentTime);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Atom actual_type = None;
|
||||
int actual_format = 0;
|
||||
unsigned long item_count = 0;
|
||||
unsigned long bytes_after = 0;
|
||||
unsigned char* data = nullptr;
|
||||
if (XGetWindowProperty(display, window, XA_PRIMARY, 0, LONG_MAX / 4,
|
||||
False, AnyPropertyType, &actual_type,
|
||||
&actual_format, &item_count, &bytes_after,
|
||||
&data) == Success) {
|
||||
if (data) {
|
||||
result.assign(reinterpret_cast<char*>(data), item_count);
|
||||
XFree(data);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Clipboard::SetText(const std::string& text) {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (!display) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to open X display");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Window window = XCreateSimpleWindow(
|
||||
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
|
||||
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||
const Atom utf8 = XInternAtom(display, "UTF8_STRING", False);
|
||||
const Atom targets = XInternAtom(display, "TARGETS", False);
|
||||
XSetSelectionOwner(display, clipboard, window, CurrentTime);
|
||||
if (XGetSelectionOwner(display, clipboard) != window) {
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return false;
|
||||
}
|
||||
|
||||
XChangeProperty(display, window, XA_PRIMARY, utf8, 8, PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*>(text.data()),
|
||||
static_cast<int>(text.size()));
|
||||
while (true) {
|
||||
XEvent event = {};
|
||||
XNextEvent(display, &event);
|
||||
if (event.type == SelectionClear) break;
|
||||
if (event.type != SelectionRequest) continue;
|
||||
|
||||
const XSelectionRequestEvent& request = event.xselectionrequest;
|
||||
XSelectionEvent response = {};
|
||||
response.type = SelectionNotify;
|
||||
response.display = request.display;
|
||||
response.requestor = request.requestor;
|
||||
response.selection = request.selection;
|
||||
response.time = request.time;
|
||||
response.target = request.target;
|
||||
response.property = request.property;
|
||||
if (request.target == targets) {
|
||||
Atom supported[] = {utf8, XA_STRING, targets};
|
||||
XChangeProperty(display, request.requestor, request.property, XA_ATOM,
|
||||
32, PropModeReplace,
|
||||
reinterpret_cast<unsigned char*>(supported), 3);
|
||||
} else if (request.target == utf8 || request.target == XA_STRING) {
|
||||
XChangeProperty(display, request.requestor, request.property,
|
||||
request.target, 8, PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*>(text.data()),
|
||||
static_cast<int>(text.size()));
|
||||
} else {
|
||||
response.property = None;
|
||||
}
|
||||
XSendEvent(display, request.requestor, False, 0,
|
||||
reinterpret_cast<XEvent*>(&response));
|
||||
XSync(display, False);
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Clipboard::HasText() {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (!display) return false;
|
||||
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||
Window owner = XGetSelectionOwner(display, clipboard);
|
||||
if (owner == None) owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||
XCloseDisplay(display);
|
||||
return owner != None;
|
||||
}
|
||||
|
||||
namespace platform {
|
||||
|
||||
void RunClipboardMonitor(int check_interval_ms) {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (!display) {
|
||||
LOG_ERROR("Failed to open X display for clipboard monitoring");
|
||||
return;
|
||||
}
|
||||
|
||||
int event_base = 0;
|
||||
int error_base = 0;
|
||||
if (!XFixesQueryExtension(display, &event_base, &error_base)) {
|
||||
LOG_WARN("XFixes extension not available, falling back to polling");
|
||||
XCloseDisplay(display);
|
||||
while (ClipboardMonitoring()) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(check_interval_ms > 0 ? check_interval_ms
|
||||
: 100));
|
||||
if (ClipboardMonitoring()) HandleClipboardChange();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||
const Window event_window = XCreateSimpleWindow(
|
||||
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
|
||||
XFixesSelectSelectionInput(display, event_window, clipboard,
|
||||
XFixesSetSelectionOwnerNotifyMask |
|
||||
XFixesSelectionWindowDestroyNotifyMask |
|
||||
XFixesSelectionClientCloseNotifyMask);
|
||||
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
|
||||
|
||||
while (ClipboardMonitoring()) {
|
||||
while (ClipboardMonitoring() && XPending(display) > 0) {
|
||||
XEvent event = {};
|
||||
XNextEvent(display, &event);
|
||||
if (event.type == event_base + XFixesSelectionNotify) {
|
||||
HandleClipboardChange();
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
|
||||
XFixesSelectSelectionInput(display, event_window, clipboard, 0);
|
||||
XDestroyWindow(display, event_window);
|
||||
XCloseDisplay(display);
|
||||
}
|
||||
|
||||
void WakeClipboardMonitor() {}
|
||||
|
||||
} // namespace platform
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,404 @@
|
||||
#include "linux_cursor_shape.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xcursor/Xcursor.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr LinuxCursorAlias kCursorAliases[] = {
|
||||
{"help", RemoteCursorShape::help},
|
||||
{"question-arrow", RemoteCursorShape::help},
|
||||
{"left-ptr-help", RemoteCursorShape::help},
|
||||
{"whats-this", RemoteCursorShape::help},
|
||||
{"dnd-ask", RemoteCursorShape::help},
|
||||
{"pointer", RemoteCursorShape::pointer},
|
||||
{"hand", RemoteCursorShape::pointer},
|
||||
{"hand1", RemoteCursorShape::pointer},
|
||||
{"hand2", RemoteCursorShape::pointer},
|
||||
{"link", RemoteCursorShape::pointer},
|
||||
{"pointing-hand", RemoteCursorShape::pointer},
|
||||
{"progress", RemoteCursorShape::progress},
|
||||
{"left-ptr-watch", RemoteCursorShape::progress},
|
||||
{"half-busy", RemoteCursorShape::progress},
|
||||
{"wait", RemoteCursorShape::wait},
|
||||
{"watch", RemoteCursorShape::wait},
|
||||
{"crosshair", RemoteCursorShape::crosshair},
|
||||
{"cross", RemoteCursorShape::crosshair},
|
||||
{"cross-reverse", RemoteCursorShape::crosshair},
|
||||
{"tcross", RemoteCursorShape::crosshair},
|
||||
{"cell", RemoteCursorShape::crosshair},
|
||||
{"plus", RemoteCursorShape::crosshair},
|
||||
{"target", RemoteCursorShape::crosshair},
|
||||
{"dotbox", RemoteCursorShape::crosshair},
|
||||
{"dot-box-mask", RemoteCursorShape::crosshair},
|
||||
{"text", RemoteCursorShape::text},
|
||||
{"xterm", RemoteCursorShape::text},
|
||||
{"ibeam", RemoteCursorShape::text},
|
||||
{"vertical-text", RemoteCursorShape::text},
|
||||
{"alias", RemoteCursorShape::alias},
|
||||
{"dnd-link", RemoteCursorShape::alias},
|
||||
{"copy", RemoteCursorShape::copy},
|
||||
{"dnd-copy", RemoteCursorShape::copy},
|
||||
{"move", RemoteCursorShape::move},
|
||||
{"fleur", RemoteCursorShape::move},
|
||||
{"size-all", RemoteCursorShape::move},
|
||||
{"all-scroll", RemoteCursorShape::move},
|
||||
{"dnd-move", RemoteCursorShape::move},
|
||||
{"no-drop", RemoteCursorShape::no_drop},
|
||||
{"dnd-no-drop", RemoteCursorShape::no_drop},
|
||||
{"dnd-none", RemoteCursorShape::no_drop},
|
||||
{"not-allowed", RemoteCursorShape::not_allowed},
|
||||
{"forbidden", RemoteCursorShape::not_allowed},
|
||||
{"crossed-circle", RemoteCursorShape::not_allowed},
|
||||
{"circle", RemoteCursorShape::not_allowed},
|
||||
{"pirate", RemoteCursorShape::not_allowed},
|
||||
{"grab", RemoteCursorShape::grab},
|
||||
{"openhand", RemoteCursorShape::grab},
|
||||
{"open-hand", RemoteCursorShape::grab},
|
||||
{"grabbing", RemoteCursorShape::grabbing},
|
||||
{"closedhand", RemoteCursorShape::grabbing},
|
||||
{"closed-hand", RemoteCursorShape::grabbing},
|
||||
{"col-resize", RemoteCursorShape::col_resize},
|
||||
{"row-resize", RemoteCursorShape::row_resize},
|
||||
{"n-resize", RemoteCursorShape::n_resize},
|
||||
{"top-side", RemoteCursorShape::n_resize},
|
||||
{"top-tee", RemoteCursorShape::n_resize},
|
||||
{"sb-up-arrow", RemoteCursorShape::n_resize},
|
||||
{"e-resize", RemoteCursorShape::e_resize},
|
||||
{"right-side", RemoteCursorShape::e_resize},
|
||||
{"right-tee", RemoteCursorShape::e_resize},
|
||||
{"sb-right-arrow", RemoteCursorShape::e_resize},
|
||||
{"s-resize", RemoteCursorShape::s_resize},
|
||||
{"bottom-side", RemoteCursorShape::s_resize},
|
||||
{"bottom-tee", RemoteCursorShape::s_resize},
|
||||
{"sb-down-arrow", RemoteCursorShape::s_resize},
|
||||
{"w-resize", RemoteCursorShape::w_resize},
|
||||
{"left-side", RemoteCursorShape::w_resize},
|
||||
{"left-tee", RemoteCursorShape::w_resize},
|
||||
{"sb-left-arrow", RemoteCursorShape::w_resize},
|
||||
{"ne-resize", RemoteCursorShape::ne_resize},
|
||||
{"top-right-corner", RemoteCursorShape::ne_resize},
|
||||
{"ur-angle", RemoteCursorShape::ne_resize},
|
||||
{"nw-resize", RemoteCursorShape::nw_resize},
|
||||
{"top-left-corner", RemoteCursorShape::nw_resize},
|
||||
{"ul-angle", RemoteCursorShape::nw_resize},
|
||||
{"se-resize", RemoteCursorShape::se_resize},
|
||||
{"bottom-right-corner", RemoteCursorShape::se_resize},
|
||||
{"lr-angle", RemoteCursorShape::se_resize},
|
||||
{"sw-resize", RemoteCursorShape::sw_resize},
|
||||
{"bottom-left-corner", RemoteCursorShape::sw_resize},
|
||||
{"ll-angle", RemoteCursorShape::sw_resize},
|
||||
{"ew-resize", RemoteCursorShape::ew_resize},
|
||||
{"sb-h-double-arrow", RemoteCursorShape::ew_resize},
|
||||
{"h-double-arrow", RemoteCursorShape::ew_resize},
|
||||
{"size-hor", RemoteCursorShape::ew_resize},
|
||||
{"split-h", RemoteCursorShape::ew_resize},
|
||||
{"exchange", RemoteCursorShape::ew_resize},
|
||||
{"ns-resize", RemoteCursorShape::ns_resize},
|
||||
{"sb-v-double-arrow", RemoteCursorShape::ns_resize},
|
||||
{"v-double-arrow", RemoteCursorShape::ns_resize},
|
||||
{"double-arrow", RemoteCursorShape::ns_resize},
|
||||
{"size-ver", RemoteCursorShape::ns_resize},
|
||||
{"split-v", RemoteCursorShape::ns_resize},
|
||||
{"nesw-resize", RemoteCursorShape::nesw_resize},
|
||||
{"fd-double-arrow", RemoteCursorShape::nesw_resize},
|
||||
{"size-bdiag", RemoteCursorShape::nesw_resize},
|
||||
{"nwse-resize", RemoteCursorShape::nwse_resize},
|
||||
{"bd-double-arrow", RemoteCursorShape::nwse_resize},
|
||||
{"size-fdiag", RemoteCursorShape::nwse_resize},
|
||||
{"default", RemoteCursorShape::default_cursor},
|
||||
{"left-ptr", RemoteCursorShape::default_cursor},
|
||||
{"left-arrow", RemoteCursorShape::default_cursor},
|
||||
{"arrow", RemoteCursorShape::default_cursor},
|
||||
{"top-left-arrow", RemoteCursorShape::default_cursor},
|
||||
{"right-ptr", RemoteCursorShape::default_cursor},
|
||||
{"center-ptr", RemoteCursorShape::default_cursor},
|
||||
{"context-menu", RemoteCursorShape::default_cursor},
|
||||
};
|
||||
|
||||
std::string NormalizeCursorName(const std::string& raw_name) {
|
||||
std::string normalized;
|
||||
normalized.reserve(raw_name.size());
|
||||
for (unsigned char ch : raw_name) {
|
||||
if (ch == '_' || std::isspace(ch)) {
|
||||
normalized.push_back('-');
|
||||
} else {
|
||||
normalized.push_back(static_cast<char>(std::tolower(ch)));
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
struct CursorCandidate {
|
||||
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t xhot = 0;
|
||||
uint32_t yhot = 0;
|
||||
std::vector<uint32_t> argb;
|
||||
};
|
||||
|
||||
uint8_t Alpha(uint32_t argb) {
|
||||
return static_cast<uint8_t>((argb >> 24U) & 0xffU);
|
||||
}
|
||||
|
||||
double BilinearAlpha(const CursorCandidate& candidate, double x, double y) {
|
||||
if (candidate.width == 0 || candidate.height == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
x = std::max(0.0, std::min(x, static_cast<double>(candidate.width - 1)));
|
||||
y = std::max(0.0, std::min(y, static_cast<double>(candidate.height - 1)));
|
||||
const uint32_t x0 = static_cast<uint32_t>(std::floor(x));
|
||||
const uint32_t y0 = static_cast<uint32_t>(std::floor(y));
|
||||
const uint32_t x1 = std::min(x0 + 1, candidate.width - 1);
|
||||
const uint32_t y1 = std::min(y0 + 1, candidate.height - 1);
|
||||
const double fx = x - x0;
|
||||
const double fy = y - y0;
|
||||
|
||||
const auto sample = [&](uint32_t px, uint32_t py) {
|
||||
return static_cast<double>(
|
||||
Alpha(candidate.argb[static_cast<size_t>(py) * candidate.width + px]));
|
||||
};
|
||||
const double top = sample(x0, y0) * (1.0 - fx) + sample(x1, y0) * fx;
|
||||
const double bottom = sample(x0, y1) * (1.0 - fx) + sample(x1, y1) * fx;
|
||||
return top * (1.0 - fy) + bottom * fy;
|
||||
}
|
||||
|
||||
double SimilarityError(const LinuxCursorImageView& image,
|
||||
const CursorCandidate& candidate) {
|
||||
if (!image.argb || image.width == 0 || image.height == 0 ||
|
||||
candidate.argb.empty()) {
|
||||
return std::numeric_limits<double>::infinity();
|
||||
}
|
||||
|
||||
double alpha_error = 0.0;
|
||||
for (uint32_t y = 0; y < image.height; ++y) {
|
||||
const double source_y =
|
||||
(static_cast<double>(y) + 0.5) * candidate.height / image.height - 0.5;
|
||||
for (uint32_t x = 0; x < image.width; ++x) {
|
||||
const double source_x = (static_cast<double>(x) + 0.5) *
|
||||
candidate.width / image.width -
|
||||
0.5;
|
||||
const double expected = BilinearAlpha(candidate, source_x, source_y);
|
||||
const double actual = static_cast<double>(
|
||||
Alpha(image.argb[static_cast<size_t>(y) * image.width + x]));
|
||||
alpha_error += std::abs(actual - expected) / 255.0;
|
||||
}
|
||||
}
|
||||
alpha_error /= static_cast<double>(image.width) * image.height;
|
||||
|
||||
const double observed_hot_x =
|
||||
static_cast<double>(image.xhot) / std::max(1u, image.width);
|
||||
const double observed_hot_y =
|
||||
static_cast<double>(image.yhot) / std::max(1u, image.height);
|
||||
const double candidate_hot_x =
|
||||
static_cast<double>(candidate.xhot) / std::max(1u, candidate.width);
|
||||
const double candidate_hot_y =
|
||||
static_cast<double>(candidate.yhot) / std::max(1u, candidate.height);
|
||||
const double hotspot_error =
|
||||
std::abs(observed_hot_x - candidate_hot_x) +
|
||||
std::abs(observed_hot_y - candidate_hot_y);
|
||||
|
||||
const double observed_aspect =
|
||||
static_cast<double>(image.width) / image.height;
|
||||
const double candidate_aspect =
|
||||
static_cast<double>(candidate.width) / candidate.height;
|
||||
const double aspect_error =
|
||||
std::abs(std::log(observed_aspect / candidate_aspect));
|
||||
|
||||
return alpha_error + hotspot_error * 0.12 + aspect_error * 0.08;
|
||||
}
|
||||
|
||||
bool SameImage(const LinuxCursorImageView& image,
|
||||
const CursorCandidate& candidate) {
|
||||
if (image.width != candidate.width || image.height != candidate.height ||
|
||||
image.xhot != candidate.xhot || image.yhot != candidate.yhot) {
|
||||
return false;
|
||||
}
|
||||
const size_t pixel_count =
|
||||
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
|
||||
return std::equal(image.argb, image.argb + pixel_count,
|
||||
candidate.argb.begin());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
const LinuxCursorAlias* LinuxCursorAliases(size_t* count) {
|
||||
if (count) {
|
||||
*count = sizeof(kCursorAliases) / sizeof(kCursorAliases[0]);
|
||||
}
|
||||
return kCursorAliases;
|
||||
}
|
||||
|
||||
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& raw_name) {
|
||||
const std::string name = NormalizeCursorName(raw_name);
|
||||
for (const auto& alias : kCursorAliases) {
|
||||
if (name == alias.name) {
|
||||
return alias.shape;
|
||||
}
|
||||
}
|
||||
|
||||
// Some toolkits prefix a theme or toolkit name to the standard cursor name.
|
||||
// Restrict the fallback to separators so names such as "pointer-progress"
|
||||
// still prefer their exact semantic alias above.
|
||||
for (const auto& alias : kCursorAliases) {
|
||||
const std::string suffix = std::string("-") + alias.name;
|
||||
if (name.size() > suffix.size() &&
|
||||
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
|
||||
return alias.shape;
|
||||
}
|
||||
}
|
||||
return RemoteCursorShape::default_cursor;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
struct LinuxCursorThemeMatcher::Impl {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
std::string theme;
|
||||
int default_size = 24;
|
||||
std::unordered_map<int, std::vector<CursorCandidate>> candidates_by_size;
|
||||
|
||||
Impl() {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
if (const char* active_theme = XcursorGetTheme(display)) {
|
||||
theme = active_theme;
|
||||
}
|
||||
const int active_size = XcursorGetDefaultSize(display);
|
||||
if (active_size > 0) {
|
||||
default_size = active_size;
|
||||
}
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
if (display) {
|
||||
XCloseDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<CursorCandidate>& Candidates(int requested_size) {
|
||||
requested_size = std::max(1, requested_size);
|
||||
auto existing = candidates_by_size.find(requested_size);
|
||||
if (existing != candidates_by_size.end()) {
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
std::vector<CursorCandidate> candidates;
|
||||
const char* theme_name = theme.empty() ? nullptr : theme.c_str();
|
||||
for (const auto& alias : kCursorAliases) {
|
||||
XcursorImages* images =
|
||||
XcursorLibraryLoadImages(alias.name, theme_name, requested_size);
|
||||
if (!images) {
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < images->nimage; ++index) {
|
||||
const XcursorImage* image = images->images[index];
|
||||
if (!image || !image->pixels || image->width == 0 ||
|
||||
image->height == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CursorCandidate candidate;
|
||||
candidate.shape = alias.shape;
|
||||
candidate.width = image->width;
|
||||
candidate.height = image->height;
|
||||
candidate.xhot = image->xhot;
|
||||
candidate.yhot = image->yhot;
|
||||
const size_t pixel_count = static_cast<size_t>(image->width) *
|
||||
static_cast<size_t>(image->height);
|
||||
candidate.argb.assign(image->pixels, image->pixels + pixel_count);
|
||||
|
||||
const bool duplicate =
|
||||
std::any_of(candidates.begin(), candidates.end(),
|
||||
[&](const CursorCandidate& current) {
|
||||
return current.shape == candidate.shape &&
|
||||
current.width == candidate.width &&
|
||||
current.height == candidate.height &&
|
||||
current.xhot == candidate.xhot &&
|
||||
current.yhot == candidate.yhot &&
|
||||
current.argb == candidate.argb;
|
||||
});
|
||||
if (!duplicate) {
|
||||
candidates.push_back(std::move(candidate));
|
||||
}
|
||||
}
|
||||
XcursorImagesDestroy(images);
|
||||
}
|
||||
|
||||
return candidates_by_size
|
||||
.emplace(requested_size, std::move(candidates))
|
||||
.first->second;
|
||||
}
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
struct LinuxCursorThemeMatcher::Impl {};
|
||||
|
||||
#endif
|
||||
|
||||
LinuxCursorThemeMatcher::LinuxCursorThemeMatcher()
|
||||
: impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
LinuxCursorThemeMatcher::~LinuxCursorThemeMatcher() = default;
|
||||
|
||||
RemoteCursorShape LinuxCursorThemeMatcher::Match(
|
||||
const LinuxCursorImageView& image) {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (!impl_ || !image.argb || image.width == 0 || image.height == 0) {
|
||||
return RemoteCursorShape::default_cursor;
|
||||
}
|
||||
|
||||
const int requested_size =
|
||||
static_cast<int>(std::max(image.width, image.height));
|
||||
const auto& candidates = impl_->Candidates(requested_size);
|
||||
for (const auto& candidate : candidates) {
|
||||
if (SameImage(image, candidate)) {
|
||||
return candidate.shape;
|
||||
}
|
||||
}
|
||||
|
||||
double best_error = std::numeric_limits<double>::infinity();
|
||||
RemoteCursorShape best_shape = RemoteCursorShape::default_cursor;
|
||||
for (const auto& candidate : candidates) {
|
||||
const double error = SimilarityError(image, candidate);
|
||||
if (error < best_error) {
|
||||
best_error = error;
|
||||
best_shape = candidate.shape;
|
||||
}
|
||||
}
|
||||
|
||||
// Scaled theme cursors normally remain well below this threshold, while an
|
||||
// unrelated custom application cursor should fall back to the default.
|
||||
return best_error <= 0.20 ? best_shape
|
||||
: RemoteCursorShape::default_cursor;
|
||||
#else
|
||||
(void)image;
|
||||
return RemoteCursorShape::default_cursor;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
|
||||
#define CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <remote_cursor_shape.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct LinuxCursorAlias {
|
||||
const char* name;
|
||||
RemoteCursorShape shape;
|
||||
};
|
||||
|
||||
const LinuxCursorAlias* LinuxCursorAliases(size_t* count);
|
||||
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& name);
|
||||
|
||||
struct LinuxCursorImageView {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t xhot = 0;
|
||||
uint32_t yhot = 0;
|
||||
const uint32_t* argb = nullptr;
|
||||
};
|
||||
|
||||
// Matches a compositor-provided cursor bitmap against the active Xcursor
|
||||
// theme. This is needed on Wayland, where XFixes only observes XWayland
|
||||
// cursors and cannot report cursors owned by native Wayland surfaces.
|
||||
class LinuxCursorThemeMatcher {
|
||||
public:
|
||||
LinuxCursorThemeMatcher();
|
||||
~LinuxCursorThemeMatcher();
|
||||
|
||||
LinuxCursorThemeMatcher(const LinuxCursorThemeMatcher&) = delete;
|
||||
LinuxCursorThemeMatcher& operator=(const LinuxCursorThemeMatcher&) = delete;
|
||||
|
||||
RemoteCursorShape Match(const LinuxCursorImageView& image);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include <dbus/dbus.h>
|
||||
#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<SessionClosedState*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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
|
||||
@@ -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 <cstdint>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "platform/daemon_backend.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace crossdesk::platform {
|
||||
|
||||
bool PrepareDaemon() {
|
||||
const bool from_terminal =
|
||||
isatty(STDIN_FILENO) != 0 || isatty(STDOUT_FILENO) != 0;
|
||||
pid_t process = fork();
|
||||
if (process < 0) return false;
|
||||
if (process > 0) _exit(0);
|
||||
if (setsid() < 0) return false;
|
||||
|
||||
process = fork();
|
||||
if (process < 0) return false;
|
||||
if (process > 0) _exit(0);
|
||||
|
||||
umask(0);
|
||||
if (chdir("/") != 0) {
|
||||
std::cerr << "Failed to change daemon working directory to /: "
|
||||
<< std::strerror(errno) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const int null_fd = open("/dev/null", O_RDWR);
|
||||
if (null_fd >= 0) {
|
||||
dup2(null_fd, STDIN_FILENO);
|
||||
if (!from_terminal) {
|
||||
dup2(null_fd, STDOUT_FILENO);
|
||||
dup2(null_fd, STDERR_FILENO);
|
||||
}
|
||||
if (null_fd > STDERR_FILENO) close(null_fd);
|
||||
}
|
||||
|
||||
signal(SIGTERM, [](int) { g_daemon_stop_requested = 1; });
|
||||
signal(SIGINT, [](int) { g_daemon_stop_requested = 1; });
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GetDaemonExecutablePath() {
|
||||
char path[PATH_MAX] = {};
|
||||
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
return count > 0 ? std::string(path, static_cast<size_t>(count))
|
||||
: std::string();
|
||||
}
|
||||
|
||||
} // namespace crossdesk::platform
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-06-23
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LINUX_TRAY_H_
|
||||
#define _LINUX_TRAY_H_
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct LinuxTrayImpl;
|
||||
|
||||
class LinuxTray {
|
||||
public:
|
||||
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index, uint32_t exit_event_type);
|
||||
LinuxTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index);
|
||||
~LinuxTray();
|
||||
|
||||
bool MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
void ProcessEvents();
|
||||
|
||||
private:
|
||||
std::unique_ptr<LinuxTrayImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#endif // _LINUX_TRAY_H_
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "device_controller_factory.h"
|
||||
|
||||
#include "keyboard_capturer.h"
|
||||
#include "mouse_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
DeviceController* DeviceControllerFactory::Create(Device device) {
|
||||
switch (device) {
|
||||
case Mouse:
|
||||
return new PlatformMouseController();
|
||||
case Keyboard:
|
||||
return new PlatformKeyboardCapturer();
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,257 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static KeyCode ResolveX11Keycode(Display* display, int key_code,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (!display) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it != vkCodeToX11KeySym.end()) {
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display, static_cast<KeySym>(key_it->second));
|
||||
if (x11_keycode != 0) {
|
||||
return x11_keycode;
|
||||
}
|
||||
}
|
||||
|
||||
// Some controllers can preserve the physical Windows scan code even when
|
||||
// they cannot resolve a Windows virtual-key value. Xorg's evdev keycodes use
|
||||
// the Linux input code plus the protocol-reserved offset of 8.
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min_keycode = 0;
|
||||
int max_keycode = 0;
|
||||
XDisplayKeycodes(display, &min_keycode, &max_keycode);
|
||||
const int x11_keycode = evdev_keycode + 8;
|
||||
if (x11_keycode < min_keycode || x11_keycode > max_keycode) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<KeyCode>(x11_keycode);
|
||||
}
|
||||
|
||||
static KeySym NormalizeKeySym(KeySym key_sym) {
|
||||
if (key_sym >= XK_a && key_sym <= XK_z) {
|
||||
return key_sym - XK_a + XK_A;
|
||||
}
|
||||
return key_sym;
|
||||
}
|
||||
|
||||
static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
(void)display;
|
||||
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
|
||||
KeySym key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 0));
|
||||
auto key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 1));
|
||||
key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
}
|
||||
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int key_code = key_it->second;
|
||||
bool is_key_down = (event->xkey.type == KeyPress);
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
|
||||
if (g_on_key_action) {
|
||||
g_on_key_action(key_code, is_key_down, scan_code, extended, g_user_ptr);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
PlatformKeyboardCapturer::PlatformKeyboardCapturer()
|
||||
: display_(nullptr),
|
||||
root_(0),
|
||||
running_(false),
|
||||
use_wayland_portal_(false),
|
||||
wayland_init_attempted_(false),
|
||||
dbus_connection_(nullptr) {
|
||||
XInitThreads();
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
return;
|
||||
}
|
||||
|
||||
int event_base = 0;
|
||||
int error_base = 0;
|
||||
int major_version = 0;
|
||||
int minor_version = 0;
|
||||
x11_xtest_available_ =
|
||||
XTestQueryExtension(display_, &event_base, &error_base, &major_version,
|
||||
&minor_version) != 0;
|
||||
}
|
||||
|
||||
PlatformKeyboardCapturer::~PlatformKeyboardCapturer() {
|
||||
Unhook();
|
||||
CleanupWaylandPortal();
|
||||
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
x11_xtest_available_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
root_ = DefaultRootWindow(display_);
|
||||
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
|
||||
XFlush(display_);
|
||||
|
||||
running_ = true;
|
||||
const int x11_fd = ConnectionNumber(display_);
|
||||
event_thread_ = std::thread([this, x11_fd]() {
|
||||
while (running_) {
|
||||
while (running_ && XPending(display_) > 0) {
|
||||
XEvent event;
|
||||
XNextEvent(display_, &event);
|
||||
KeyboardEventHandler(display_, &event);
|
||||
}
|
||||
|
||||
if (!running_) {
|
||||
break;
|
||||
}
|
||||
|
||||
struct pollfd pfd = {x11_fd, POLLIN, 0};
|
||||
int poll_ret = poll(&pfd, 1, 50);
|
||||
if (poll_ret < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
LOG_ERROR("poll for X11 events failed.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (poll_ret == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
|
||||
LOG_ERROR("poll got invalid X11 event fd state.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((pfd.revents & POLLIN) == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PlatformKeyboardCapturer::Unhook() {
|
||||
running_ = false;
|
||||
|
||||
if (event_thread_.joinable()) {
|
||||
event_thread_.join();
|
||||
}
|
||||
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
|
||||
if (display_ && root_ != 0) {
|
||||
XSelectInput(display_, root_, 0);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (IsWaylandSession()) {
|
||||
if (!use_wayland_portal_ && !wayland_init_attempted_) {
|
||||
wayland_init_attempted_ = true;
|
||||
if (InitWaylandPortal()) {
|
||||
use_wayland_portal_ = true;
|
||||
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Wayland keyboard control init failed, falling back to X11/XTest "
|
||||
"backend");
|
||||
}
|
||||
}
|
||||
|
||||
if (use_wayland_portal_) {
|
||||
return SendWaylandKeyboardCommand(key_code, is_down, scan_code, extended);
|
||||
}
|
||||
}
|
||||
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
if (!x11_xtest_available_) {
|
||||
LOG_ERROR("XTest extension not available for keyboard injection");
|
||||
return -2;
|
||||
}
|
||||
|
||||
const KeyCode x11_keycode =
|
||||
ResolveX11Keycode(display_, key_code, scan_code, extended);
|
||||
if (x11_keycode == 0) {
|
||||
LOG_WARN(
|
||||
"Cannot map remote keyboard event to X11 keycode, vk_code={}, "
|
||||
"scan_code={}, extended={}",
|
||||
key_code, scan_code, extended);
|
||||
return -3;
|
||||
}
|
||||
|
||||
if (!XTestFakeKeyEvent(display_, x11_keycode, is_down, CurrentTime)) {
|
||||
LOG_ERROR(
|
||||
"XTest keyboard injection failed, vk_code={}, scan_code={}, "
|
||||
"extended={}, x11_keycode={}, is_down={}",
|
||||
key_code, scan_code, extended, static_cast<int>(x11_keycode), is_down);
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Complete the request before reporting success to the keyboard-state
|
||||
// reconciler. This also makes X11 protocol errors observable immediately.
|
||||
XSync(display_, False);
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-22
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CAPTURER_H_
|
||||
#define _KEYBOARD_CAPTURER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
struct _XDisplay;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class PlatformKeyboardCapturer final : public KeyboardCapturer {
|
||||
public:
|
||||
PlatformKeyboardCapturer();
|
||||
virtual ~PlatformKeyboardCapturer();
|
||||
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
bool InitWaylandPortal();
|
||||
void CleanupWaylandPortal();
|
||||
int SendWaylandKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
|
||||
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
|
||||
bool SendWaylandPortalVoidCall(const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>&
|
||||
append_args);
|
||||
|
||||
private:
|
||||
_XDisplay* display_;
|
||||
unsigned long root_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
std::mutex x11_injection_mutex_;
|
||||
bool x11_xtest_available_ = false;
|
||||
bool use_wayland_portal_ = false;
|
||||
bool wayland_init_attempted_ = false;
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
std::string wayland_session_handle_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,735 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "rd_log.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern std::map<int, int> vkCodeToX11KeySym;
|
||||
|
||||
#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* kPortalRequestInterface =
|
||||
"org.freedesktop.portal.Request";
|
||||
constexpr const char* kPortalRequestPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/request/";
|
||||
constexpr const char* kPortalSessionPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/session/";
|
||||
|
||||
constexpr uint32_t kRemoteDesktopDeviceKeyboard = 1u;
|
||||
constexpr uint32_t kKeyboardReleased = 0u;
|
||||
constexpr uint32_t kKeyboardPressed = 1u;
|
||||
|
||||
int NormalizeFallbackKeysym(int keysym) {
|
||||
if (keysym >= XK_A && keysym <= XK_Z) {
|
||||
return keysym - XK_A + XK_a;
|
||||
}
|
||||
return keysym;
|
||||
}
|
||||
|
||||
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 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<uint32_t>(temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
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) {
|
||||
(void)connection;
|
||||
auto* state = static_cast<PortalResponseState*>(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<bool(DBusMessage*)>& append_message_args,
|
||||
const std::function<bool(uint32_t, DBusMessageIter*)>& 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 PlatformKeyboardCapturer::InitWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
CleanupWaylandPortal();
|
||||
|
||||
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_keyboard_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_keyboard_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", kRemoteDesktopDeviceKeyboard);
|
||||
AppendDictEntryString(&options, "handle_token",
|
||||
MakeToken("crossdesk_keyboard_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 char* parent_window = "";
|
||||
bool keyboard_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_keyboard_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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
|
||||
keyboard_granted =
|
||||
(granted_devices & kRemoteDesktopDeviceKeyboard) != 0;
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR(
|
||||
"RemoteDesktop.Start granted devices mask={}, keyboard not allowed",
|
||||
granted_devices);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!start_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR("RemoteDesktop session started without keyboard permission");
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PlatformKeyboardCapturer::CleanupWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (dbus_connection_) {
|
||||
CloseWaylandPortalSessionAndConnection(dbus_connection_,
|
||||
wayland_session_handle_,
|
||||
"RemoteDesktop.Session.Close");
|
||||
dbus_connection_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
use_wayland_portal_ = false;
|
||||
wayland_session_handle_.clear();
|
||||
}
|
||||
|
||||
int PlatformKeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode >= 0 &&
|
||||
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
if (evdev_keycode >= 0) {
|
||||
LOG_ERROR(
|
||||
"Failed to send Wayland keyboard keycode event, vk_code={}, "
|
||||
"evdev_keycode={}, is_down={}",
|
||||
key_code, evdev_keycode, is_down);
|
||||
return -3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
if (x11_keycode > 8) {
|
||||
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int keysym = key_it->second;
|
||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Failed to send Wayland keyboard event, vk_code={}, is_down={}",
|
||||
key_code, is_down);
|
||||
return -3;
|
||||
#else
|
||||
(void)key_code;
|
||||
(void)is_down;
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PlatformKeyboardCapturer::NotifyWaylandKeyboardKeysym(int keysym, uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeysym", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_sym = keysym;
|
||||
uint32_t key_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, &key_sym);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keysym;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PlatformKeyboardCapturer::NotifyWaylandKeyboardKeycode(int keycode,
|
||||
uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeycode", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_code = keycode;
|
||||
uint32_t key_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, &key_code);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keycode;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PlatformKeyboardCapturer::SendWaylandPortalVoidCall(
|
||||
const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>& 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
|
||||
@@ -0,0 +1,295 @@
|
||||
#ifndef _LINUX_EVDEV_KEYCODE_H_
|
||||
#define _LINUX_EVDEV_KEYCODE_H_
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
|
||||
bool extended) {
|
||||
const uint32_t base_scan_code = scan_code & 0xFFu;
|
||||
if (base_scan_code == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (extended) {
|
||||
switch (base_scan_code) {
|
||||
case 0x1C:
|
||||
return KEY_KPENTER;
|
||||
case 0x1D:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0x35:
|
||||
return KEY_KPSLASH;
|
||||
case 0x38:
|
||||
return KEY_RIGHTALT;
|
||||
case 0x47:
|
||||
return KEY_HOME;
|
||||
case 0x48:
|
||||
return KEY_UP;
|
||||
case 0x49:
|
||||
return KEY_PAGEUP;
|
||||
case 0x4B:
|
||||
return KEY_LEFT;
|
||||
case 0x4D:
|
||||
return KEY_RIGHT;
|
||||
case 0x4F:
|
||||
return KEY_END;
|
||||
case 0x50:
|
||||
return KEY_DOWN;
|
||||
case 0x51:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x52:
|
||||
return KEY_INSERT;
|
||||
case 0x53:
|
||||
return KEY_DELETE;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
|
||||
// with the low byte of the Windows scan code.
|
||||
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
|
||||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
|
||||
base_scan_code == 0x58) {
|
||||
return static_cast<int>(base_scan_code);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0x08:
|
||||
return KEY_BACKSPACE;
|
||||
case 0x09:
|
||||
return KEY_TAB;
|
||||
case 0x0D:
|
||||
return KEY_ENTER;
|
||||
case 0x10:
|
||||
case 0xA0:
|
||||
return KEY_LEFTSHIFT;
|
||||
case 0x11:
|
||||
case 0xA2:
|
||||
return KEY_LEFTCTRL;
|
||||
case 0x12:
|
||||
case 0xA4:
|
||||
return KEY_LEFTALT;
|
||||
case 0x13:
|
||||
return KEY_PAUSE;
|
||||
case 0x14:
|
||||
return KEY_CAPSLOCK;
|
||||
case 0x1B:
|
||||
return KEY_ESC;
|
||||
case 0x20:
|
||||
return KEY_SPACE;
|
||||
case 0x21:
|
||||
return KEY_PAGEUP;
|
||||
case 0x22:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x23:
|
||||
return KEY_END;
|
||||
case 0x24:
|
||||
return KEY_HOME;
|
||||
case 0x25:
|
||||
return KEY_LEFT;
|
||||
case 0x26:
|
||||
return KEY_UP;
|
||||
case 0x27:
|
||||
return KEY_RIGHT;
|
||||
case 0x28:
|
||||
return KEY_DOWN;
|
||||
case 0x2C:
|
||||
return KEY_SYSRQ;
|
||||
case 0x2D:
|
||||
return KEY_INSERT;
|
||||
case 0x2E:
|
||||
return KEY_DELETE;
|
||||
case 0x30:
|
||||
return KEY_0;
|
||||
case 0x31:
|
||||
return KEY_1;
|
||||
case 0x32:
|
||||
return KEY_2;
|
||||
case 0x33:
|
||||
return KEY_3;
|
||||
case 0x34:
|
||||
return KEY_4;
|
||||
case 0x35:
|
||||
return KEY_5;
|
||||
case 0x36:
|
||||
return KEY_6;
|
||||
case 0x37:
|
||||
return KEY_7;
|
||||
case 0x38:
|
||||
return KEY_8;
|
||||
case 0x39:
|
||||
return KEY_9;
|
||||
case 0x41:
|
||||
return KEY_A;
|
||||
case 0x42:
|
||||
return KEY_B;
|
||||
case 0x43:
|
||||
return KEY_C;
|
||||
case 0x44:
|
||||
return KEY_D;
|
||||
case 0x45:
|
||||
return KEY_E;
|
||||
case 0x46:
|
||||
return KEY_F;
|
||||
case 0x47:
|
||||
return KEY_G;
|
||||
case 0x48:
|
||||
return KEY_H;
|
||||
case 0x49:
|
||||
return KEY_I;
|
||||
case 0x4A:
|
||||
return KEY_J;
|
||||
case 0x4B:
|
||||
return KEY_K;
|
||||
case 0x4C:
|
||||
return KEY_L;
|
||||
case 0x4D:
|
||||
return KEY_M;
|
||||
case 0x4E:
|
||||
return KEY_N;
|
||||
case 0x4F:
|
||||
return KEY_O;
|
||||
case 0x50:
|
||||
return KEY_P;
|
||||
case 0x51:
|
||||
return KEY_Q;
|
||||
case 0x52:
|
||||
return KEY_R;
|
||||
case 0x53:
|
||||
return KEY_S;
|
||||
case 0x54:
|
||||
return KEY_T;
|
||||
case 0x55:
|
||||
return KEY_U;
|
||||
case 0x56:
|
||||
return KEY_V;
|
||||
case 0x57:
|
||||
return KEY_W;
|
||||
case 0x58:
|
||||
return KEY_X;
|
||||
case 0x59:
|
||||
return KEY_Y;
|
||||
case 0x5A:
|
||||
return KEY_Z;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
case 0x60:
|
||||
return KEY_KP0;
|
||||
case 0x61:
|
||||
return KEY_KP1;
|
||||
case 0x62:
|
||||
return KEY_KP2;
|
||||
case 0x63:
|
||||
return KEY_KP3;
|
||||
case 0x64:
|
||||
return KEY_KP4;
|
||||
case 0x65:
|
||||
return KEY_KP5;
|
||||
case 0x66:
|
||||
return KEY_KP6;
|
||||
case 0x67:
|
||||
return KEY_KP7;
|
||||
case 0x68:
|
||||
return KEY_KP8;
|
||||
case 0x69:
|
||||
return KEY_KP9;
|
||||
case 0x6A:
|
||||
return KEY_KPASTERISK;
|
||||
case 0x6B:
|
||||
return KEY_KPPLUS;
|
||||
case 0x6D:
|
||||
return KEY_KPMINUS;
|
||||
case 0x6E:
|
||||
return KEY_KPDOT;
|
||||
case 0x6F:
|
||||
return KEY_KPSLASH;
|
||||
case 0x70:
|
||||
return KEY_F1;
|
||||
case 0x71:
|
||||
return KEY_F2;
|
||||
case 0x72:
|
||||
return KEY_F3;
|
||||
case 0x73:
|
||||
return KEY_F4;
|
||||
case 0x74:
|
||||
return KEY_F5;
|
||||
case 0x75:
|
||||
return KEY_F6;
|
||||
case 0x76:
|
||||
return KEY_F7;
|
||||
case 0x77:
|
||||
return KEY_F8;
|
||||
case 0x78:
|
||||
return KEY_F9;
|
||||
case 0x79:
|
||||
return KEY_F10;
|
||||
case 0x7A:
|
||||
return KEY_F11;
|
||||
case 0x7B:
|
||||
return KEY_F12;
|
||||
case 0x90:
|
||||
return KEY_NUMLOCK;
|
||||
case 0x91:
|
||||
return KEY_SCROLLLOCK;
|
||||
case 0xA1:
|
||||
return KEY_RIGHTSHIFT;
|
||||
case 0xA3:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0xA5:
|
||||
return KEY_RIGHTALT;
|
||||
case 0xBA:
|
||||
return KEY_SEMICOLON;
|
||||
case 0xBB:
|
||||
return KEY_EQUAL;
|
||||
case 0xBC:
|
||||
return KEY_COMMA;
|
||||
case 0xBD:
|
||||
return KEY_MINUS;
|
||||
case 0xBE:
|
||||
return KEY_DOT;
|
||||
case 0xBF:
|
||||
return KEY_SLASH;
|
||||
case 0xC0:
|
||||
return KEY_GRAVE;
|
||||
case 0xDB:
|
||||
return KEY_LEFTBRACE;
|
||||
case 0xDC:
|
||||
return KEY_BACKSLASH;
|
||||
case 0xDD:
|
||||
return KEY_RIGHTBRACE;
|
||||
case 0xDE:
|
||||
return KEY_APOSTROPHE;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
const int scan_keycode =
|
||||
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
|
||||
if (scan_keycode >= 0) {
|
||||
return scan_keycode;
|
||||
}
|
||||
|
||||
return LinuxEvdevKeycodeFromWindowsVk(key_code);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PlatformMouseController::PlatformMouseController() {}
|
||||
|
||||
PlatformMouseController::~PlatformMouseController() { Destroy(); }
|
||||
|
||||
int PlatformMouseController::Init(std::vector<DisplayInfo> 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");
|
||||
return -1;
|
||||
}
|
||||
|
||||
root_ = DefaultRootWindow(display_);
|
||||
|
||||
int event_base, error_base, major_version, minor_version;
|
||||
if (!XTestQueryExtension(display_, &event_base, &error_base, &major_version,
|
||||
&minor_version)) {
|
||||
LOG_ERROR("XTest extension not available");
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
return -2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PlatformMouseController::UpdateDisplayInfoList(
|
||||
const std::vector<DisplayInfo>& 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<int>(display_info_list_.size())) {
|
||||
last_display_index_ = -1;
|
||||
last_norm_x_ = -1.0;
|
||||
last_norm_y_ = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
int PlatformMouseController::Destroy() {
|
||||
CleanupWaylandPortal();
|
||||
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PlatformMouseController::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: {
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid display index: {}", display_index);
|
||||
return -2;
|
||||
}
|
||||
|
||||
SetMousePosition(
|
||||
static_cast<int>(remote_action.m.x *
|
||||
display_info_list_[display_index].width +
|
||||
display_info_list_[display_index].left),
|
||||
static_cast<int>(remote_action.m.y *
|
||||
display_info_list_[display_index].height +
|
||||
display_info_list_[display_index].top));
|
||||
break;
|
||||
}
|
||||
case MouseFlag::left_down:
|
||||
XTestFakeButtonEvent(display_, 1, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
XTestFakeButtonEvent(display_, 1, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
XTestFakeButtonEvent(display_, 3, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
XTestFakeButtonEvent(display_, 3, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
XTestFakeButtonEvent(display_, 2, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
XTestFakeButtonEvent(display_, 2, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical: {
|
||||
if (remote_action.m.s > 0) {
|
||||
SimulateMouseWheel(4, remote_action.m.s);
|
||||
} else if (remote_action.m.s < 0) {
|
||||
SimulateMouseWheel(5, -remote_action.m.s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseFlag::wheel_horizontal: {
|
||||
if (remote_action.m.s > 0) {
|
||||
SimulateMouseWheel(6, remote_action.m.s);
|
||||
} else if (remote_action.m.s < 0) {
|
||||
SimulateMouseWheel(7, -remote_action.m.s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PlatformMouseController::SetMousePosition(int x, int y) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void PlatformMouseController::SimulateKeyDown(int kval) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XTestFakeKeyEvent(display_, kval, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void PlatformMouseController::SimulateKeyUp(int kval) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XTestFakeKeyEvent(display_, kval, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void PlatformMouseController::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
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-05-07
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "display_info.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
struct _XDisplay;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class PlatformMouseController final : public MouseController {
|
||||
public:
|
||||
PlatformMouseController();
|
||||
virtual ~PlatformMouseController();
|
||||
|
||||
public:
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list);
|
||||
virtual int Destroy();
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
void UpdateDisplayInfoList(const std::vector<DisplayInfo>& 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<void(DBusMessageIter*)>& append_args);
|
||||
|
||||
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
|
||||
|
||||
_XDisplay* display_ = nullptr;
|
||||
unsigned long root_ = 0;
|
||||
std::vector<DisplayInfo> 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;
|
||||
int wayland_portal_space_width_ = 0;
|
||||
int wayland_portal_space_height_ = 0;
|
||||
bool using_shared_wayland_session_ = false;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
#include "platform/path_backend.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk::platform {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path Home() {
|
||||
const char* value = std::getenv("HOME");
|
||||
return value ? std::filesystem::path(value) : std::filesystem::path();
|
||||
}
|
||||
|
||||
std::filesystem::path EnvOrDefault(const char* name,
|
||||
const std::filesystem::path& fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
return value ? std::filesystem::path(value) : fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
std::vector<char> buffer(PATH_MAX);
|
||||
while (true) {
|
||||
const ssize_t length =
|
||||
readlink("/proc/self/exe", buffer.data(), buffer.size() - 1);
|
||||
if (length <= 0) return {};
|
||||
if (static_cast<size_t>(length) < buffer.size() - 1) {
|
||||
buffer[static_cast<size_t>(length)] = '\0';
|
||||
return std::filesystem::path(buffer.data()).parent_path();
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path GetConfigPath(const std::string& app_name) {
|
||||
return EnvOrDefault("XDG_CONFIG_HOME", Home() / ".config") / app_name;
|
||||
}
|
||||
|
||||
std::filesystem::path GetCachePath(const std::string& app_name) {
|
||||
return EnvOrDefault("XDG_CACHE_HOME", Home() / ".cache") / app_name;
|
||||
}
|
||||
|
||||
std::filesystem::path GetLogPath(const std::string& app_name) {
|
||||
return GetCachePath(app_name) / "logs";
|
||||
}
|
||||
|
||||
} // namespace crossdesk::platform
|
||||
@@ -0,0 +1,575 @@
|
||||
#include "screen_capturer_drm.h"
|
||||
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
|
||||
defined(__has_include) && __has_include(<xf86drm.h>) && \
|
||||
__has_include(<xf86drmMode.h>)
|
||||
#define CROSSDESK_DRM_BUILD_ENABLED 1
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
#elif defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
|
||||
defined(__has_include) && __has_include(<libdrm/xf86drm.h>) && \
|
||||
__has_include(<libdrm/xf86drmMode.h>)
|
||||
#define CROSSDESK_DRM_BUILD_ENABLED 1
|
||||
#include <libdrm/xf86drm.h>
|
||||
#include <libdrm/xf86drmMode.h>
|
||||
#else
|
||||
#define CROSSDESK_DRM_BUILD_ENABLED 0
|
||||
#endif
|
||||
|
||||
#if CROSSDESK_DRM_BUILD_ENABLED
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kMaxDrmCards = 16;
|
||||
|
||||
const char* ConnectorTypeName(uint32_t type) {
|
||||
switch (type) {
|
||||
case DRM_MODE_CONNECTOR_VGA:
|
||||
return "VGA";
|
||||
case DRM_MODE_CONNECTOR_DVII:
|
||||
return "DVI-I";
|
||||
case DRM_MODE_CONNECTOR_DVID:
|
||||
return "DVI-D";
|
||||
case DRM_MODE_CONNECTOR_DVIA:
|
||||
return "DVI-A";
|
||||
case DRM_MODE_CONNECTOR_HDMIA:
|
||||
return "HDMI-A";
|
||||
case DRM_MODE_CONNECTOR_HDMIB:
|
||||
return "HDMI-B";
|
||||
case DRM_MODE_CONNECTOR_DisplayPort:
|
||||
return "DP";
|
||||
case DRM_MODE_CONNECTOR_eDP:
|
||||
return "eDP";
|
||||
case DRM_MODE_CONNECTOR_LVDS:
|
||||
return "LVDS";
|
||||
#ifdef DRM_MODE_CONNECTOR_VIRTUAL
|
||||
case DRM_MODE_CONNECTOR_VIRTUAL:
|
||||
return "Virtual";
|
||||
#endif
|
||||
default:
|
||||
return "Display";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerDrm::ScreenCapturerDrm() {}
|
||||
|
||||
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
|
||||
|
||||
int ScreenCapturerDrm::Init(const int fps, cb_desktop_data cb) {
|
||||
Destroy();
|
||||
|
||||
if (!cb) {
|
||||
LOG_ERROR("DRM screen capturer callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fps_ = std::max(1, fps);
|
||||
callback_ = cb;
|
||||
monitor_index_ = 0;
|
||||
initial_monitor_index_ = 0;
|
||||
consecutive_failures_ = 0;
|
||||
display_info_list_.clear();
|
||||
outputs_.clear();
|
||||
y_plane_.clear();
|
||||
uv_plane_.clear();
|
||||
|
||||
if (!DiscoverOutputs()) {
|
||||
LOG_ERROR("DRM screen capturer could not find active outputs");
|
||||
callback_ = nullptr;
|
||||
CloseDevices();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Destroy() {
|
||||
Stop();
|
||||
callback_ = nullptr;
|
||||
display_info_list_.clear();
|
||||
outputs_.clear();
|
||||
y_plane_.clear();
|
||||
uv_plane_.clear();
|
||||
CloseDevices();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Start(bool show_cursor) {
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (outputs_.empty()) {
|
||||
LOG_ERROR("DRM screen capturer has no output to capture");
|
||||
return -1;
|
||||
}
|
||||
|
||||
show_cursor_ = show_cursor;
|
||||
paused_ = false;
|
||||
|
||||
int probe_index = monitor_index_.load();
|
||||
if (probe_index < 0 || probe_index >= static_cast<int>(outputs_.size())) {
|
||||
probe_index = 0;
|
||||
}
|
||||
|
||||
if (!CaptureOutputFrame(outputs_[probe_index], false)) {
|
||||
LOG_ERROR("DRM start probe failed on output {}", outputs_[probe_index].name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
thread_ = std::thread([this]() { CaptureLoop(); });
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Stop() {
|
||||
if (!running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::SwitchTo(int monitor_index) {
|
||||
if (monitor_index < 0 ||
|
||||
monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid DRM monitor index: {}", monitor_index);
|
||||
return -1;
|
||||
}
|
||||
|
||||
monitor_index_ = monitor_index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::ResetToInitialMonitor() {
|
||||
monitor_index_ = initial_monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDrm::DiscoverOutputs() {
|
||||
for (int card_index = 0; card_index < kMaxDrmCards; ++card_index) {
|
||||
const std::string card_path = "/dev/dri/card" + std::to_string(card_index);
|
||||
const int fd = open(card_path.c_str(), O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drmModeRes* resources = drmModeGetResources(fd);
|
||||
if (!resources) {
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
DrmDevice device;
|
||||
device.fd = fd;
|
||||
device.path = card_path;
|
||||
devices_.push_back(device);
|
||||
const int device_slot = static_cast<int>(devices_.size()) - 1;
|
||||
const size_t output_count_before = outputs_.size();
|
||||
|
||||
for (int i = 0; i < resources->count_connectors; ++i) {
|
||||
drmModeConnector* connector =
|
||||
drmModeGetConnector(fd, resources->connectors[i]);
|
||||
if (!connector) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (connector->connection != DRM_MODE_CONNECTED ||
|
||||
connector->count_modes <= 0) {
|
||||
drmModeFreeConnector(connector);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t crtc_id = 0;
|
||||
if (connector->encoder_id != 0) {
|
||||
drmModeEncoder* encoder = drmModeGetEncoder(fd, connector->encoder_id);
|
||||
if (encoder) {
|
||||
crtc_id = encoder->crtc_id;
|
||||
drmModeFreeEncoder(encoder);
|
||||
}
|
||||
}
|
||||
|
||||
if (crtc_id == 0) {
|
||||
for (int enc_idx = 0; enc_idx < connector->count_encoders; ++enc_idx) {
|
||||
drmModeEncoder* encoder =
|
||||
drmModeGetEncoder(fd, connector->encoders[enc_idx]);
|
||||
if (!encoder) {
|
||||
continue;
|
||||
}
|
||||
if (encoder->crtc_id != 0) {
|
||||
crtc_id = encoder->crtc_id;
|
||||
drmModeFreeEncoder(encoder);
|
||||
break;
|
||||
}
|
||||
drmModeFreeEncoder(encoder);
|
||||
}
|
||||
}
|
||||
|
||||
if (crtc_id == 0) {
|
||||
drmModeFreeConnector(connector);
|
||||
continue;
|
||||
}
|
||||
|
||||
drmModeCrtc* crtc = drmModeGetCrtc(fd, crtc_id);
|
||||
if (!crtc || !crtc->mode_valid || crtc->width <= 0 || crtc->height <= 0) {
|
||||
if (crtc) {
|
||||
drmModeFreeCrtc(crtc);
|
||||
}
|
||||
drmModeFreeConnector(connector);
|
||||
continue;
|
||||
}
|
||||
|
||||
DrmOutput output;
|
||||
output.device_index = device_slot;
|
||||
output.connector_id = connector->connector_id;
|
||||
output.crtc_id = crtc_id;
|
||||
output.left = crtc->x;
|
||||
output.top = crtc->y;
|
||||
output.width = static_cast<int>(crtc->width);
|
||||
output.height = static_cast<int>(crtc->height);
|
||||
output.name = std::string(ConnectorTypeName(connector->connector_type)) +
|
||||
std::to_string(connector->connector_type_id);
|
||||
output.stream_id = MakeDisplayStreamId(outputs_.size());
|
||||
|
||||
outputs_.push_back(output);
|
||||
display_info_list_.push_back(
|
||||
DisplayInfo(output.name, output.left, output.top,
|
||||
output.left + output.width, output.top + output.height));
|
||||
|
||||
LOG_INFO("DRM output found: {} on {}, {}x{} @ ({}, {})", output.name,
|
||||
card_path, output.width, output.height, output.left, output.top);
|
||||
|
||||
drmModeFreeCrtc(crtc);
|
||||
drmModeFreeConnector(connector);
|
||||
}
|
||||
|
||||
drmModeFreeResources(resources);
|
||||
|
||||
if (outputs_.size() == output_count_before) {
|
||||
close(fd);
|
||||
devices_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
if (outputs_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("DRM screen capturer discovered {} output(s)", outputs_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerDrm::CloseDevices() {
|
||||
for (auto& device : devices_) {
|
||||
if (device.fd >= 0) {
|
||||
close(device.fd);
|
||||
device.fd = -1;
|
||||
}
|
||||
}
|
||||
devices_.clear();
|
||||
}
|
||||
|
||||
void ScreenCapturerDrm::CaptureLoop() {
|
||||
using clock = std::chrono::steady_clock;
|
||||
const auto frame_interval =
|
||||
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
|
||||
|
||||
while (running_) {
|
||||
const auto frame_start = clock::now();
|
||||
if (!paused_) {
|
||||
int index = monitor_index_.load();
|
||||
if (index >= 0 && index < static_cast<int>(outputs_.size())) {
|
||||
const bool ok = CaptureOutputFrame(outputs_[index], true);
|
||||
if (!ok) {
|
||||
++consecutive_failures_;
|
||||
if (consecutive_failures_ == 1 || consecutive_failures_ % 60 == 0) {
|
||||
LOG_WARN("DRM capture failed (consecutive={})",
|
||||
consecutive_failures_);
|
||||
}
|
||||
} else {
|
||||
consecutive_failures_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
clock::now() - frame_start);
|
||||
if (elapsed < frame_interval) {
|
||||
std::this_thread::sleep_for(frame_interval - elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
|
||||
bool emit_callback) {
|
||||
if (output.device_index < 0 ||
|
||||
output.device_index >= static_cast<int>(devices_.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int fd = devices_[output.device_index].fd;
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
drmModeCrtc* crtc = drmModeGetCrtc(fd, output.crtc_id);
|
||||
if (!crtc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t fb_id = crtc->buffer_id;
|
||||
drmModeFreeCrtc(crtc);
|
||||
if (fb_id == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
drmModeFB* fb = drmModeGetFB(fd, fb_id);
|
||||
if (!fb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t handle = fb->handle;
|
||||
const uint32_t pitch = fb->pitch;
|
||||
const int src_width = static_cast<int>(fb->width);
|
||||
const int src_height = static_cast<int>(fb->height);
|
||||
const int bpp = static_cast<int>(fb->bpp);
|
||||
drmModeFreeFB(fb);
|
||||
|
||||
if (handle == 0 || pitch == 0 || src_width <= 1 || src_height <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bpp != 32) {
|
||||
LOG_WARN("DRM capture unsupported bpp: {}", bpp);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t map_size =
|
||||
static_cast<size_t>(pitch) * static_cast<size_t>(src_height);
|
||||
uint8_t* mapped_ptr = nullptr;
|
||||
size_t mapped_size = 0;
|
||||
int prime_fd = -1;
|
||||
if (!MapFramebuffer(fd, handle, map_size, &mapped_ptr, &mapped_size,
|
||||
&prime_fd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int capture_width = std::min(src_width, output.width);
|
||||
int capture_height = std::min(src_height, output.height);
|
||||
if (capture_width <= 0 || capture_height <= 0) {
|
||||
capture_width = src_width;
|
||||
capture_height = src_height;
|
||||
}
|
||||
|
||||
capture_width &= ~1;
|
||||
capture_height &= ~1;
|
||||
if (capture_width <= 1 || capture_height <= 1) {
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(capture_width) * static_cast<size_t>(capture_height);
|
||||
const size_t uv_size = y_size / 2;
|
||||
if (y_plane_.size() != y_size) {
|
||||
y_plane_.resize(y_size);
|
||||
}
|
||||
if (uv_plane_.size() != uv_size) {
|
||||
uv_plane_.resize(uv_size);
|
||||
}
|
||||
|
||||
const int convert_ret =
|
||||
libyuv::ARGBToNV12(mapped_ptr, static_cast<int>(pitch), y_plane_.data(),
|
||||
capture_width, uv_plane_.data(), capture_width,
|
||||
capture_width, capture_height);
|
||||
|
||||
if (convert_ret != 0) {
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> nv12;
|
||||
nv12.reserve(y_plane_.size() + uv_plane_.size());
|
||||
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
|
||||
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
|
||||
|
||||
if (emit_callback && callback_) {
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
|
||||
capture_height, output.stream_id.c_str());
|
||||
}
|
||||
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDrm::MapFramebuffer(int fd, uint32_t handle, size_t map_size,
|
||||
uint8_t** mapped_ptr,
|
||||
size_t* mapped_size,
|
||||
int* prime_fd) const {
|
||||
if (!mapped_ptr || !mapped_size || !prime_fd || map_size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*mapped_ptr = nullptr;
|
||||
*mapped_size = 0;
|
||||
*prime_fd = -1;
|
||||
|
||||
drm_mode_map_dumb map_arg{};
|
||||
map_arg.handle = handle;
|
||||
if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg) == 0) {
|
||||
void* mapped = mmap(nullptr, map_size, PROT_READ, MAP_SHARED, fd,
|
||||
static_cast<off_t>(map_arg.offset));
|
||||
if (mapped != MAP_FAILED) {
|
||||
*mapped_ptr = static_cast<uint8_t*>(mapped);
|
||||
*mapped_size = map_size;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int dma_fd = -1;
|
||||
if (drmPrimeHandleToFD(fd, handle, DRM_CLOEXEC, &dma_fd) == 0) {
|
||||
size_t dma_map_size = map_size;
|
||||
const off_t fd_size = lseek(dma_fd, 0, SEEK_END);
|
||||
if (fd_size > 0) {
|
||||
dma_map_size = std::min(map_size, static_cast<size_t>(fd_size));
|
||||
}
|
||||
|
||||
void* mapped =
|
||||
mmap(nullptr, dma_map_size, PROT_READ, MAP_SHARED, dma_fd, 0);
|
||||
if (mapped != MAP_FAILED) {
|
||||
*mapped_ptr = static_cast<uint8_t*>(mapped);
|
||||
*mapped_size = dma_map_size;
|
||||
*prime_fd = dma_fd;
|
||||
return true;
|
||||
}
|
||||
|
||||
close(dma_fd);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerDrm::UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
|
||||
int prime_fd) const {
|
||||
if (mapped_ptr && mapped_size > 0) {
|
||||
munmap(mapped_ptr, mapped_size);
|
||||
}
|
||||
|
||||
if (prime_fd >= 0) {
|
||||
close(prime_fd);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#else
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
ScreenCapturerDrm::ScreenCapturerDrm() {}
|
||||
|
||||
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
|
||||
|
||||
int ScreenCapturerDrm::Init([[maybe_unused]] const int fps, cb_desktop_data cb) {
|
||||
Destroy();
|
||||
callback_ = cb;
|
||||
LOG_WARN("DRM screen capturer disabled: libdrm headers not available");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Destroy() {
|
||||
Stop();
|
||||
callback_ = nullptr;
|
||||
display_info_list_.clear();
|
||||
outputs_.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Start([[maybe_unused]] bool show_cursor) { return -1; }
|
||||
|
||||
int ScreenCapturerDrm::Stop() {
|
||||
running_ = false;
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) { return 0; }
|
||||
|
||||
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) { return 0; }
|
||||
|
||||
int ScreenCapturerDrm::SwitchTo([[maybe_unused]] int monitor_index) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerDrm::ResetToInitialMonitor() { return 0; }
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDrm::DiscoverOutputs() { return false; }
|
||||
|
||||
void ScreenCapturerDrm::CloseDevices() {}
|
||||
|
||||
void ScreenCapturerDrm::CaptureLoop() {}
|
||||
|
||||
bool ScreenCapturerDrm::CaptureOutputFrame(
|
||||
[[maybe_unused]] const DrmOutput& output,
|
||||
[[maybe_unused]] bool emit_callback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDrm::MapFramebuffer([[maybe_unused]] int fd,
|
||||
[[maybe_unused]] uint32_t handle,
|
||||
[[maybe_unused]] size_t map_size,
|
||||
[[maybe_unused]] uint8_t** mapped_ptr,
|
||||
[[maybe_unused]] size_t* mapped_size,
|
||||
[[maybe_unused]] int* prime_fd) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerDrm::UnmapFramebuffer([[maybe_unused]] uint8_t* mapped_ptr,
|
||||
[[maybe_unused]] size_t mapped_size,
|
||||
[[maybe_unused]] int prime_fd) const {}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-03-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_DRM_H_
|
||||
#define _SCREEN_CAPTURER_DRM_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerDrm : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerDrm();
|
||||
~ScreenCapturerDrm();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
private:
|
||||
struct DrmDevice {
|
||||
int fd = -1;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DrmOutput {
|
||||
int device_index = -1;
|
||||
uint32_t connector_id = 0;
|
||||
uint32_t crtc_id = 0;
|
||||
std::string name;
|
||||
std::string stream_id;
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
bool DiscoverOutputs();
|
||||
void CloseDevices();
|
||||
void CaptureLoop();
|
||||
bool CaptureOutputFrame(const DrmOutput& output, bool emit_callback = true);
|
||||
bool MapFramebuffer(int fd, uint32_t handle, size_t map_size,
|
||||
uint8_t** mapped_ptr, size_t* mapped_size,
|
||||
int* prime_fd) const;
|
||||
void UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
|
||||
int prime_fd) const;
|
||||
|
||||
private:
|
||||
std::vector<DrmDevice> devices_;
|
||||
std::vector<DrmOutput> outputs_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
int consecutive_failures_ = 0;
|
||||
|
||||
std::vector<uint8_t> y_plane_;
|
||||
std::vector<uint8_t> uv_plane_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "screen_capturer_factory.h"
|
||||
|
||||
#include "screen_capturer_linux.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
ScreenCapturer* ScreenCapturerFactory::Create() {
|
||||
return new ScreenCapturerLinux();
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,576 @@
|
||||
#include "screen_capturer_linux.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
#include "screen_capturer_drm.h"
|
||||
#endif
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include "screen_capturer_wayland.h"
|
||||
#endif
|
||||
#include "screen_capturer_x11.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
constexpr bool kDrmBuildEnabled = true;
|
||||
#else
|
||||
constexpr bool kDrmBuildEnabled = false;
|
||||
#endif
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
constexpr bool kWaylandBuildEnabled = true;
|
||||
#else
|
||||
constexpr bool kWaylandBuildEnabled = false;
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerLinux::ScreenCapturerLinux() {}
|
||||
|
||||
ScreenCapturerLinux::~ScreenCapturerLinux() { Destroy(); }
|
||||
|
||||
int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
Destroy();
|
||||
|
||||
if (!cb) {
|
||||
LOG_ERROR("Linux screen capturer callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fps_ = fps;
|
||||
callback_orig_ = std::move(cb);
|
||||
callback_ = [this](unsigned char* data, int size, int width, int height,
|
||||
const char* reported_stream_id) {
|
||||
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
LOG_WARN("Linux capturer dropping frame without a registered stream "
|
||||
"id: reported='{}', size={}x{}, bytes={}",
|
||||
reported_stream_id ? reported_stream_id : "", width, height,
|
||||
size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
if (callback_orig_) {
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
const char* force_backend = getenv("CROSSDESK_SCREEN_BACKEND");
|
||||
if (force_backend && force_backend[0] != '\0') {
|
||||
if (strcmp(force_backend, "drm") == 0) {
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
LOG_INFO("Linux screen capturer forced backend: DRM");
|
||||
return InitDrm();
|
||||
#else
|
||||
LOG_ERROR(
|
||||
"Linux screen capturer forced backend DRM is disabled at build time");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (strcmp(force_backend, "x11") == 0) {
|
||||
LOG_INFO("Linux screen capturer forced backend: X11");
|
||||
return InitX11();
|
||||
}
|
||||
if (strcmp(force_backend, "wayland") == 0) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
LOG_INFO("Linux screen capturer forced backend: Wayland");
|
||||
return InitWayland();
|
||||
#else
|
||||
LOG_ERROR(
|
||||
"Linux screen capturer forced backend Wayland is disabled at build "
|
||||
"time");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
LOG_WARN("Unknown CROSSDESK_SCREEN_BACKEND={}, using auto strategy",
|
||||
force_backend);
|
||||
}
|
||||
|
||||
const bool wayland_session = IsWaylandSession();
|
||||
if (wayland_session) {
|
||||
if (kDrmBuildEnabled) {
|
||||
LOG_INFO("Wayland session detected, prefer DRM -> X11 -> Wayland");
|
||||
if (InitDrm() == 0) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Wayland session detected, DRM disabled, prefer X11 -> Wayland");
|
||||
}
|
||||
|
||||
if (InitX11() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (kDrmBuildEnabled) {
|
||||
LOG_WARN(
|
||||
"DRM and X11 init failed in Wayland session, trying Wayland portal");
|
||||
} else {
|
||||
LOG_WARN("X11 init failed in Wayland session, trying Wayland portal");
|
||||
}
|
||||
if (kWaylandBuildEnabled) {
|
||||
return InitWayland();
|
||||
}
|
||||
LOG_ERROR("Wayland session detected but Wayland backend is disabled");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (InitX11() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (kDrmBuildEnabled) {
|
||||
LOG_WARN("X11 init failed, trying DRM fallback");
|
||||
return InitDrm();
|
||||
}
|
||||
|
||||
LOG_ERROR("X11 init failed and DRM backend is disabled");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Destroy() {
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
impl_.reset();
|
||||
}
|
||||
|
||||
backend_ = BackendType::kNone;
|
||||
callback_ = nullptr;
|
||||
callback_orig_ = nullptr;
|
||||
current_monitor_index_.store(0, std::memory_order_relaxed);
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
canonical_displays_.clear();
|
||||
stream_id_alias_.clear();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Start(bool show_cursor) {
|
||||
if (!impl_) {
|
||||
LOG_ERROR("Linux screen capturer backend is not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// X11 output names, DRM connectors and Wayland stream handles may all change
|
||||
// after an HDMI hotplug, so rebuild the current backend before each session.
|
||||
const int refresh_ret = RefreshCurrentBackend();
|
||||
if (refresh_ret != 0) {
|
||||
LOG_WARN("Linux screen capturer backend refresh failed: {}", refresh_ret);
|
||||
}
|
||||
|
||||
const int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* backend_name = "None";
|
||||
if (backend_ == BackendType::kX11) {
|
||||
backend_name = "X11";
|
||||
} else if (backend_ == BackendType::kDrm) {
|
||||
backend_name = "DRM";
|
||||
} else if (backend_ == BackendType::kWayland) {
|
||||
backend_name = "Wayland";
|
||||
}
|
||||
|
||||
LOG_WARN("Linux screen capturer backend {} start failed: {}", backend_name,
|
||||
ret);
|
||||
|
||||
if (backend_ == BackendType::kX11 && kDrmBuildEnabled &&
|
||||
TryFallbackToDrm(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
if (backend_ == BackendType::kX11 && kWaylandBuildEnabled &&
|
||||
TryFallbackToWayland(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
if (backend_ == BackendType::kDrm && kDrmBuildEnabled) {
|
||||
if (TryFallbackToX11(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
if (kWaylandBuildEnabled && TryFallbackToWayland(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (backend_ == BackendType::kWayland && kWaylandBuildEnabled) {
|
||||
if (kDrmBuildEnabled && TryFallbackToDrm(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
if (TryFallbackToX11(show_cursor)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Stop() {
|
||||
if (!impl_) {
|
||||
return 0;
|
||||
}
|
||||
const int ret = impl_->Stop();
|
||||
UpdateAliasesFromBackend(impl_.get());
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Pause(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
return impl_->Pause(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Resume(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
return impl_->Resume(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::SwitchTo(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
const int ret = impl_->SwitchTo(monitor_index);
|
||||
if (ret == 0) {
|
||||
current_monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::ResetToInitialMonitor() {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
const int ret = impl_->ResetToInitialMonitor();
|
||||
if (ret == 0) {
|
||||
current_monitor_index_.store(initial_monitor_index_,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
|
||||
if (!impl_) {
|
||||
return std::vector<DisplayInfo>();
|
||||
}
|
||||
|
||||
// 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<std::mutex> lock(alias_mutex_);
|
||||
if (!canonical_displays_.empty()) {
|
||||
return canonical_displays_;
|
||||
}
|
||||
|
||||
return impl_->GetDisplayInfoList();
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::InitX11() {
|
||||
auto backend = std::make_unique<ScreenCapturerX11>();
|
||||
const int ret = backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
backend->Destroy();
|
||||
LOG_WARN("Linux screen capturer X11 init failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
impl_ = std::move(backend);
|
||||
backend_ = BackendType::kX11;
|
||||
LOG_INFO("Linux screen capturer backend selected: X11");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::InitDrm() {
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
auto backend = std::make_unique<ScreenCapturerDrm>();
|
||||
const int ret = backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
backend->Destroy();
|
||||
LOG_WARN("Linux screen capturer DRM init failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
impl_ = std::move(backend);
|
||||
backend_ = BackendType::kDrm;
|
||||
LOG_INFO("Linux screen capturer backend selected: DRM");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WARN("Linux screen capturer DRM backend is disabled at build time");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::InitWayland() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
auto backend = std::make_unique<ScreenCapturerWayland>();
|
||||
const int ret = backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
backend->Destroy();
|
||||
LOG_WARN("Linux screen capturer Wayland init failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
impl_ = std::move(backend);
|
||||
backend_ = BackendType::kWayland;
|
||||
LOG_INFO("Linux screen capturer backend selected: Wayland");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WARN("Linux screen capturer Wayland backend is disabled at build time");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::RefreshCurrentBackend() {
|
||||
std::unique_ptr<ScreenCapturer> backend;
|
||||
const char* backend_name = "unknown";
|
||||
switch (backend_) {
|
||||
case BackendType::kX11:
|
||||
backend = std::make_unique<ScreenCapturerX11>();
|
||||
backend_name = "X11";
|
||||
break;
|
||||
case BackendType::kDrm:
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
backend = std::make_unique<ScreenCapturerDrm>();
|
||||
backend_name = "DRM";
|
||||
break;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
case BackendType::kWayland:
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
backend = std::make_unique<ScreenCapturerWayland>();
|
||||
backend_name = "Wayland";
|
||||
break;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
case BackendType::kNone:
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int ret = backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
backend->Destroy();
|
||||
return ret;
|
||||
}
|
||||
|
||||
const int requested_monitor =
|
||||
current_monitor_index_.load(std::memory_order_relaxed);
|
||||
UpdateAliasesFromBackend(backend.get());
|
||||
if (requested_monitor > 0 && backend->SwitchTo(requested_monitor) != 0) {
|
||||
current_monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
}
|
||||
|
||||
impl_ = std::move(backend);
|
||||
LOG_INFO("Linux screen capturer {} backend refreshed before start",
|
||||
backend_name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
auto drm_backend = std::make_unique<ScreenCapturerDrm>();
|
||||
int ret = drm_backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Linux screen capturer fallback DRM init failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(drm_backend.get());
|
||||
ret = drm_backend->Start(show_cursor);
|
||||
if (ret != 0) {
|
||||
drm_backend->Destroy();
|
||||
LOG_ERROR("Linux screen capturer fallback DRM start failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
impl_->Stop();
|
||||
impl_->Destroy();
|
||||
}
|
||||
|
||||
impl_ = std::move(drm_backend);
|
||||
backend_ = BackendType::kDrm;
|
||||
LOG_INFO("Linux screen capturer fallback switched to DRM");
|
||||
return true;
|
||||
#else
|
||||
(void)show_cursor;
|
||||
LOG_WARN("Linux screen capturer DRM fallback is disabled at build time");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ScreenCapturerLinux::TryFallbackToX11(bool show_cursor) {
|
||||
auto x11_backend = std::make_unique<ScreenCapturerX11>();
|
||||
int ret = x11_backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Linux screen capturer fallback X11 init failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(x11_backend.get());
|
||||
ret = x11_backend->Start(show_cursor);
|
||||
if (ret != 0) {
|
||||
x11_backend->Destroy();
|
||||
LOG_ERROR("Linux screen capturer fallback X11 start failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
impl_->Stop();
|
||||
impl_->Destroy();
|
||||
}
|
||||
|
||||
impl_ = std::move(x11_backend);
|
||||
backend_ = BackendType::kX11;
|
||||
LOG_INFO("Linux screen capturer fallback switched to X11");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerLinux::TryFallbackToWayland(bool show_cursor) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
auto wayland_backend = std::make_unique<ScreenCapturerWayland>();
|
||||
int ret = wayland_backend->Init(fps_, callback_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Linux screen capturer fallback Wayland init failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateAliasesFromBackend(wayland_backend.get());
|
||||
ret = wayland_backend->Start(show_cursor);
|
||||
if (ret != 0) {
|
||||
wayland_backend->Destroy();
|
||||
LOG_ERROR("Linux screen capturer fallback Wayland start failed: {}", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
impl_->Stop();
|
||||
impl_->Destroy();
|
||||
}
|
||||
|
||||
impl_ = std::move(wayland_backend);
|
||||
backend_ = BackendType::kWayland;
|
||||
LOG_INFO("Linux screen capturer fallback switched to Wayland");
|
||||
return true;
|
||||
#else
|
||||
(void)show_cursor;
|
||||
LOG_WARN("Linux screen capturer Wayland fallback is disabled at build time");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
|
||||
if (!backend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto backend_displays = backend->GetDisplayInfoList();
|
||||
if (backend_displays.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
stream_id_alias_.clear();
|
||||
|
||||
if (canonical_displays_.empty()) {
|
||||
canonical_displays_ = backend_displays;
|
||||
for (size_t i = 0; i < backend_displays.size(); ++i) {
|
||||
auto& canonical = canonical_displays_[i];
|
||||
const std::string stream_id = MakeDisplayStreamId(i);
|
||||
if (canonical.name.empty()) {
|
||||
canonical.name = stream_id;
|
||||
}
|
||||
stream_id_alias_[stream_id] = stream_id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (canonical_displays_.size() < backend_displays.size()) {
|
||||
LOG_WARN("Linux capturer detected {} additional display(s); they remain "
|
||||
"unavailable until peer streams are reinitialized",
|
||||
backend_displays.size() - canonical_displays_.size());
|
||||
}
|
||||
|
||||
auto similar = [](const DisplayInfo& current, const DisplayInfo& canonical) {
|
||||
return std::abs(current.left - canonical.left) <= 10 &&
|
||||
std::abs(current.top - canonical.top) <= 10 &&
|
||||
std::abs(current.width - canonical.width) <= 20 &&
|
||||
std::abs(current.height - canonical.height) <= 20;
|
||||
};
|
||||
std::vector<bool> used(canonical_displays_.size(), false);
|
||||
for (size_t current_index = 0; current_index < backend_displays.size();
|
||||
++current_index) {
|
||||
const auto& backend_display = backend_displays[current_index];
|
||||
int canonical_index = -1;
|
||||
for (size_t i = 0; i < canonical_displays_.size(); ++i) {
|
||||
if (!used[i] && similar(backend_display, canonical_displays_[i])) {
|
||||
canonical_index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canonical_index < 0 && current_index < canonical_displays_.size() &&
|
||||
!used[current_index]) {
|
||||
canonical_index = static_cast<int>(current_index);
|
||||
}
|
||||
if (canonical_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
used[canonical_index] = true;
|
||||
const std::string backend_stream_id =
|
||||
MakeDisplayStreamId(current_index);
|
||||
const std::string stable_stream_id =
|
||||
MakeDisplayStreamId(static_cast<size_t>(canonical_index));
|
||||
stream_id_alias_[backend_stream_id] = stable_stream_id;
|
||||
|
||||
// Refresh the user-visible label and geometry. The MiniRTC stream ID is
|
||||
// derived independently from this stable logical index.
|
||||
canonical_displays_[canonical_index] = backend_display;
|
||||
if (canonical_displays_[canonical_index].name.empty()) {
|
||||
canonical_displays_[canonical_index].name = stable_stream_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ScreenCapturerLinux::MapStreamId(
|
||||
const char* reported_stream_id) const {
|
||||
std::string input_id = reported_stream_id ? reported_stream_id : "";
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = stream_id_alias_.find(input_id);
|
||||
if (it != stream_id_alias_.end()) {
|
||||
input_id = it->second;
|
||||
} else {
|
||||
input_id.clear();
|
||||
}
|
||||
|
||||
return ResolveDisplayStreamId(
|
||||
input_id.c_str(), canonical_displays_.size(),
|
||||
current_monitor_index_.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-03-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_LINUX_H_
|
||||
#define _SCREEN_CAPTURER_LINUX_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerLinux : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerLinux();
|
||||
~ScreenCapturerLinux();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
private:
|
||||
enum class BackendType { kNone, kX11, kDrm, kWayland };
|
||||
|
||||
private:
|
||||
int InitX11();
|
||||
int InitDrm();
|
||||
int InitWayland();
|
||||
int RefreshCurrentBackend();
|
||||
bool TryFallbackToDrm(bool show_cursor);
|
||||
bool TryFallbackToX11(bool show_cursor);
|
||||
bool TryFallbackToWayland(bool show_cursor);
|
||||
void UpdateAliasesFromBackend(ScreenCapturer* backend);
|
||||
std::string MapStreamId(const char* reported_stream_id) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
BackendType backend_ = BackendType::kNone;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
cb_desktop_data callback_orig_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
mutable std::mutex alias_mutex_;
|
||||
std::unordered_map<std::string, std::string> stream_id_alias_;
|
||||
std::atomic<int> current_monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
mutable std::atomic<bool> invalid_stream_id_logged_{false};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "screen_capturer_wayland.h"
|
||||
|
||||
#include "screen_capturer_wayland_build.h"
|
||||
|
||||
#if !CROSSDESK_WAYLAND_BUILD_ENABLED
|
||||
#error \
|
||||
"Wayland capturer requires USE_WAYLAND=true and Wayland development headers"
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include "linux_cursor_shape.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "shared_cursor_state.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
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()
|
||||
: cursor_theme_matcher_(std::make_unique<LinuxCursorThemeMatcher>()) {}
|
||||
|
||||
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
|
||||
|
||||
int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
|
||||
Destroy();
|
||||
|
||||
if (!IsWaylandSession()) {
|
||||
LOG_ERROR("Wayland screen capturer requires a Wayland session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cb) {
|
||||
LOG_ERROR("Wayland screen capturer callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!CheckPortalAvailability()) {
|
||||
LOG_ERROR("xdg-desktop-portal screencast service is unavailable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!EnsurePipeWireRuntimeAvailable()) {
|
||||
LOG_ERROR("Wayland screen capturer requires PipeWire 0.3 runtime");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fps_ = fps;
|
||||
callback_ = cb;
|
||||
pointer_granted_ = false;
|
||||
shared_session_registered_ = false;
|
||||
cursor_metadata_enabled_ = false;
|
||||
cursor_metadata_seen_ = false;
|
||||
cursor_metadata_missing_buffers_ = 0;
|
||||
ClearSharedCursorState();
|
||||
display_info_list_.clear();
|
||||
display_info_list_.push_back(
|
||||
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
|
||||
monitor_index_ = 0;
|
||||
initial_monitor_index_ = 0;
|
||||
frame_width_ = kFallbackWidth;
|
||||
frame_height_ = kFallbackHeight;
|
||||
frame_stride_ = kFallbackWidth * 4;
|
||||
portal_has_logical_size_ = false;
|
||||
portal_stream_width_ = 0;
|
||||
portal_stream_height_ = 0;
|
||||
logical_width_ = kFallbackWidth;
|
||||
logical_height_ = kFallbackHeight;
|
||||
y_plane_.resize(kFallbackWidth * kFallbackHeight);
|
||||
uv_plane_.resize((kFallbackWidth / 2) * (kFallbackHeight / 2) * 2);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::Destroy() {
|
||||
Stop();
|
||||
y_plane_.clear();
|
||||
uv_plane_.clear();
|
||||
display_info_list_.clear();
|
||||
callback_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::Start(bool show_cursor) {
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
show_cursor_ = show_cursor;
|
||||
cursor_metadata_enabled_ = false;
|
||||
cursor_metadata_seen_ = false;
|
||||
cursor_metadata_missing_buffers_ = 0;
|
||||
ClearSharedCursorState();
|
||||
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;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::Stop() {
|
||||
running_ = false;
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
pipewire_node_id_ = 0;
|
||||
cursor_metadata_enabled_ = false;
|
||||
cursor_metadata_seen_ = false;
|
||||
cursor_metadata_missing_buffers_ = 0;
|
||||
ClearSharedCursorState();
|
||||
UpdateDisplayGeometry(
|
||||
logical_width_ > 0 ? logical_width_ : kFallbackWidth,
|
||||
logical_height_ > 0 ? logical_height_ : kFallbackHeight);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::Pause([[maybe_unused]] int monitor_index) {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::Resume([[maybe_unused]] int monitor_index) {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::SwitchTo(int monitor_index) {
|
||||
if (monitor_index != 0) {
|
||||
LOG_WARN("Wayland screencast currently supports one logical display");
|
||||
return -1;
|
||||
}
|
||||
|
||||
monitor_index_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWayland::ResetToInitialMonitor() {
|
||||
monitor_index_ = initial_monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerWayland::GetDisplayInfoList() {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::Run() {
|
||||
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<int>(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<int>(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
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-03-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_WAYLAND_H_
|
||||
#define _SCREEN_CAPTURER_WAYLAND_H_
|
||||
|
||||
struct DBusConnection;
|
||||
struct pw_context;
|
||||
struct pw_core;
|
||||
struct pw_stream;
|
||||
struct pw_thread_loop;
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class LinuxCursorThemeMatcher;
|
||||
|
||||
class ScreenCapturerWayland : public ScreenCapturer {
|
||||
public:
|
||||
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
|
||||
|
||||
public:
|
||||
ScreenCapturerWayland();
|
||||
~ScreenCapturerWayland();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
private:
|
||||
bool CheckPortalAvailability() const;
|
||||
bool ConnectSessionBus();
|
||||
bool CreatePortalSession();
|
||||
bool SelectPortalDevices();
|
||||
bool SelectPortalSource();
|
||||
bool GetAvailableCursorModes(uint32_t* modes) const;
|
||||
bool StartPortalSession();
|
||||
bool EnsurePipeWireRuntimeAvailable() const;
|
||||
bool OpenPipeWireRemote();
|
||||
bool SetupPipeWireStream(bool relaxed_connect, PipeWireConnectMode mode);
|
||||
|
||||
void Run();
|
||||
void CleanupPipeWire();
|
||||
void CleanupDbus();
|
||||
void ClosePortalSession();
|
||||
void HandlePipeWireBuffer();
|
||||
void UpdateDisplayGeometry(int width, int height);
|
||||
|
||||
private:
|
||||
static constexpr int kFallbackWidth = 1920;
|
||||
static constexpr int kFallbackHeight = 1080;
|
||||
|
||||
std::thread thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
std::atomic<bool> pipewire_format_ready_{false};
|
||||
std::atomic<int64_t> pipewire_stream_start_ms_{0};
|
||||
std::atomic<int64_t> pipewire_last_frame_ms_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
std::string session_handle_;
|
||||
std::string display_name_ = "WAYLAND0";
|
||||
uint32_t pipewire_node_id_ = 0;
|
||||
int pipewire_fd_ = -1;
|
||||
|
||||
pw_thread_loop* pw_thread_loop_ = nullptr;
|
||||
pw_context* pw_context_ = nullptr;
|
||||
pw_core* pw_core_ = nullptr;
|
||||
pw_stream* pw_stream_ = nullptr;
|
||||
void* stream_listener_ = nullptr;
|
||||
bool pipewire_initialized_ = false;
|
||||
bool pipewire_thread_loop_started_ = false;
|
||||
bool pointer_granted_ = false;
|
||||
bool shared_session_registered_ = false;
|
||||
bool cursor_metadata_enabled_ = false;
|
||||
bool cursor_metadata_seen_ = false;
|
||||
uint32_t cursor_metadata_missing_buffers_ = 0;
|
||||
bool portal_has_logical_size_ = false;
|
||||
uint32_t spa_video_format_ = 0;
|
||||
int frame_width_ = 0;
|
||||
int frame_height_ = 0;
|
||||
int frame_stride_ = 0;
|
||||
int portal_stream_width_ = 0;
|
||||
int portal_stream_height_ = 0;
|
||||
int logical_width_ = 0;
|
||||
int logical_height_ = 0;
|
||||
std::unique_ptr<LinuxCursorThemeMatcher> cursor_theme_matcher_;
|
||||
|
||||
std::vector<uint8_t> y_plane_;
|
||||
std::vector<uint8_t> uv_plane_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-03-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_WAYLAND_BUILD_H_
|
||||
#define _SCREEN_CAPTURER_WAYLAND_BUILD_H_
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
|
||||
#define CROSSDESK_WAYLAND_BUILD_ENABLED 1
|
||||
|
||||
#include <dbus/dbus.h>
|
||||
#include <pipewire/keys.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/stream.h>
|
||||
#include <pipewire/thread-loop.h>
|
||||
#include <spa/param/param.h>
|
||||
#include <spa/param/format-utils.h>
|
||||
#include <spa/param/video/format-utils.h>
|
||||
#include <spa/param/video/raw.h>
|
||||
#include <spa/buffer/meta.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<spa/param/buffers.h>)
|
||||
#include <spa/param/buffers.h>
|
||||
#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
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+1083
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
||||
#include "screen_capturer_wayland.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
#include "screen_capturer_wayland_build.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
#if CROSSDESK_WAYLAND_BUILD_ENABLED
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "shared_cursor_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
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 kScreenCastSourceMonitor = 1u;
|
||||
constexpr uint32_t kCursorModeHidden = 1u;
|
||||
constexpr uint32_t kCursorModeEmbedded = 2u;
|
||||
constexpr uint32_t kCursorModeMetadata = 4u;
|
||||
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
bool ReadIntLike(DBusMessageIter* iter, int* value) {
|
||||
if (!iter || !value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int type = dbus_message_iter_get_arg_type(iter);
|
||||
if (type == DBUS_TYPE_INT32) {
|
||||
int32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
*value = static_cast<int>(temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == DBUS_TYPE_UINT32) {
|
||||
uint32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
*value = static_cast<int>(temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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<PortalResponseState*>(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,
|
||||
const std::atomic<bool>& running,
|
||||
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 (running.load() && !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<bool(DBusMessage*)>& append_message_args,
|
||||
const std::atomic<bool>& running,
|
||||
const std::function<bool(uint32_t, DBusMessageIter*)>& 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, running);
|
||||
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
|
||||
|
||||
bool ScreenCapturerWayland::CheckPortalAvailability() const {
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
|
||||
DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
|
||||
if (!connection) {
|
||||
LogDbusError("dbus_bus_get", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbus_bool_t has_owner =
|
||||
dbus_bus_name_has_owner(connection, kPortalBusName, &error);
|
||||
if (dbus_error_is_set(&error)) {
|
||||
LogDbusError("dbus_bus_name_has_owner", &error);
|
||||
dbus_error_free(&error);
|
||||
dbus_connection_unref(connection);
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_connection_unref(connection);
|
||||
return has_owner == TRUE;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::ConnectSessionBus() {
|
||||
if (dbus_connection_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::CreatePortalSession() {
|
||||
if (!dbus_connection_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string session_handle_token = MakeToken("crossdesk_session");
|
||||
std::string request_path;
|
||||
const bool 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_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
running_,
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("CreateSession was denied or malformed, 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()) {
|
||||
session_handle_ = parsed_handle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
&request_path);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session_handle_.empty()) {
|
||||
const std::string fallback_handle =
|
||||
BuildSessionHandleFromRequestPath(request_path, session_handle_token);
|
||||
if (!fallback_handle.empty()) {
|
||||
LOG_WARN(
|
||||
"CreateSession response missing session_handle, using derived handle "
|
||||
"{}",
|
||||
fallback_handle);
|
||||
session_handle_ = fallback_handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (session_handle_.empty()) {
|
||||
LOG_ERROR("CreateSession response did not include a session handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::SelectPortalSource() {
|
||||
if (!dbus_connection_ || session_handle_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t available_cursor_modes = kCursorModeHidden | kCursorModeEmbedded;
|
||||
if (!GetAvailableCursorModes(&available_cursor_modes)) {
|
||||
LOG_WARN(
|
||||
"Unable to query portal cursor modes; using embedded cursor fallback");
|
||||
}
|
||||
|
||||
uint32_t cursor_mode = 0;
|
||||
cursor_metadata_enabled_ = false;
|
||||
if (!show_cursor_ && (available_cursor_modes & kCursorModeMetadata) != 0) {
|
||||
cursor_mode = kCursorModeMetadata;
|
||||
cursor_metadata_enabled_ = true;
|
||||
} else if ((available_cursor_modes & kCursorModeEmbedded) != 0) {
|
||||
cursor_mode = kCursorModeEmbedded;
|
||||
} else if ((available_cursor_modes & kCursorModeMetadata) != 0) {
|
||||
cursor_mode = kCursorModeMetadata;
|
||||
cursor_metadata_enabled_ = true;
|
||||
} else if ((available_cursor_modes & kCursorModeHidden) != 0) {
|
||||
cursor_mode = kCursorModeHidden;
|
||||
}
|
||||
|
||||
if (cursor_mode == 0) {
|
||||
LOG_ERROR("Wayland portal reports no supported cursor mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cursor_mode == kCursorModeEmbedded) {
|
||||
// When metadata is unavailable, embedding is the only way to preserve the
|
||||
// exact Wayland cursor. Hide the controller-side cursor to avoid a double
|
||||
// cursor over the video stream.
|
||||
PublishSharedCursorState(false, RemoteCursorShape::none);
|
||||
} else if (!cursor_metadata_enabled_) {
|
||||
// A portal without metadata or embedded cursor support cannot expose the
|
||||
// native Wayland shape. Keep a visible default cursor on the controller.
|
||||
PublishSharedCursorState(true, RemoteCursorShape::default_cursor);
|
||||
}
|
||||
|
||||
LOG_INFO("Wayland portal cursor mode: {}",
|
||||
cursor_mode == kCursorModeMetadata
|
||||
? "metadata"
|
||||
: (cursor_mode == kCursorModeEmbedded ? "embedded" : "hidden"));
|
||||
|
||||
const char* session_handle = session_handle_.c_str();
|
||||
return 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);
|
||||
AppendDictEntryUint32(&options, "cursor_mode", cursor_mode);
|
||||
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("SelectSources was denied or malformed, response={}",
|
||||
response_code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::GetAvailableCursorModes(uint32_t* modes) const {
|
||||
if (!dbus_connection_ || !modes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, kPortalObjectPath, "org.freedesktop.DBus.Properties",
|
||||
"Get");
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* interface_name = kPortalScreenCastInterface;
|
||||
const char* property_name = "AvailableCursorModes";
|
||||
dbus_message_append_args(message, DBUS_TYPE_STRING, &interface_name,
|
||||
DBUS_TYPE_STRING, &property_name,
|
||||
DBUS_TYPE_INVALID);
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
|
||||
dbus_connection_, message, -1, &error);
|
||||
dbus_message_unref(message);
|
||||
if (!reply) {
|
||||
LogDbusError("Get AvailableCursorModes", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter iter;
|
||||
bool parsed = false;
|
||||
if (dbus_message_iter_init(reply, &iter) &&
|
||||
dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_VARIANT) {
|
||||
DBusMessageIter variant;
|
||||
dbus_message_iter_recurse(&iter, &variant);
|
||||
if (dbus_message_iter_get_arg_type(&variant) == DBUS_TYPE_UINT32) {
|
||||
dbus_message_iter_get_basic(&variant, modes);
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
dbus_message_unref(reply);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const char* session_handle = session_handle_.c_str();
|
||||
const char* parent_window = "";
|
||||
pointer_granted_ = false;
|
||||
const bool 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_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
running_,
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("Start was denied or malformed, response={}",
|
||||
response_code);
|
||||
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) {
|
||||
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) {
|
||||
int granted_devices_int = 0;
|
||||
if (ReadIntLike(&variant, &granted_devices_int) &&
|
||||
granted_devices_int >= 0) {
|
||||
granted_devices = static_cast<uint32_t>(granted_devices_int);
|
||||
}
|
||||
} else if (strcmp(key, "streams") == 0) {
|
||||
DBusMessageIter streams;
|
||||
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 (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);
|
||||
|
||||
portal_stream_width_ = stream_width;
|
||||
portal_stream_height_ = stream_height;
|
||||
portal_has_logical_size_ =
|
||||
logical_width > 0 && logical_height > 0;
|
||||
|
||||
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(&dict);
|
||||
}
|
||||
pointer_granted_ = (granted_devices & kRemoteDesktopDevicePointer) != 0;
|
||||
return true;
|
||||
});
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pipewire_node_id_ == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::OpenPipeWireRemote() {
|
||||
if (!dbus_connection_ || session_handle_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, kPortalObjectPath, kPortalScreenCastInterface,
|
||||
"OpenPipeWireRemote");
|
||||
if (!message) {
|
||||
LOG_ERROR("Failed to allocate OpenPipeWireRemote message");
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter iter;
|
||||
DBusMessageIter options;
|
||||
const char* session_handle = session_handle_.c_str();
|
||||
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);
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
|
||||
dbus_connection_, message, -1, &error);
|
||||
dbus_message_unref(message);
|
||||
if (!reply) {
|
||||
LogDbusError("OpenPipeWireRemote", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter reply_iter;
|
||||
if (!dbus_message_iter_init(reply, &reply_iter) ||
|
||||
dbus_message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_UNIX_FD) {
|
||||
LOG_ERROR("OpenPipeWireRemote returned an unexpected payload");
|
||||
dbus_message_unref(reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
int received_fd = -1;
|
||||
dbus_message_iter_get_basic(&reply_iter, &received_fd);
|
||||
dbus_message_unref(reply);
|
||||
|
||||
if (received_fd < 0) {
|
||||
LOG_ERROR("OpenPipeWireRemote returned an invalid fd");
|
||||
return false;
|
||||
}
|
||||
|
||||
pipewire_fd_ = dup(received_fd);
|
||||
if (pipewire_fd_ < 0) {
|
||||
LOG_ERROR("Failed to duplicate PipeWire remote fd");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::CleanupDbus() {
|
||||
if (!dbus_connection_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shared_session_registered_) {
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_connection_close(dbus_connection_);
|
||||
dbus_connection_unref(dbus_connection_);
|
||||
dbus_connection_ = nullptr;
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::ClosePortalSession() {
|
||||
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");
|
||||
}
|
||||
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
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,437 @@
|
||||
#include "screen_capturer_x11.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<int> g_x11_last_error_code{0};
|
||||
std::mutex g_x11_error_handler_mutex;
|
||||
|
||||
int CaptureX11ErrorHandler([[maybe_unused]] Display* display,
|
||||
XErrorEvent* error_event) {
|
||||
if (error_event) {
|
||||
g_x11_last_error_code.store(error_event->error_code);
|
||||
} else {
|
||||
g_x11_last_error_code.store(-1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
class ScopedX11ErrorTrap {
|
||||
public:
|
||||
explicit ScopedX11ErrorTrap(Display* display)
|
||||
: display_(display), lock_(g_x11_error_handler_mutex) {
|
||||
g_x11_last_error_code.store(0);
|
||||
previous_handler_ = XSetErrorHandler(CaptureX11ErrorHandler);
|
||||
}
|
||||
|
||||
~ScopedX11ErrorTrap() {
|
||||
if (display_) {
|
||||
XSync(display_, False);
|
||||
}
|
||||
XSetErrorHandler(previous_handler_);
|
||||
}
|
||||
|
||||
int SyncAndGetError() const {
|
||||
if (display_) {
|
||||
XSync(display_, False);
|
||||
}
|
||||
return g_x11_last_error_code.load();
|
||||
}
|
||||
|
||||
private:
|
||||
Display* display_ = nullptr;
|
||||
int (*previous_handler_)(Display*, XErrorEvent*) = nullptr;
|
||||
std::unique_lock<std::mutex> lock_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerX11::ScreenCapturerX11() {}
|
||||
|
||||
ScreenCapturerX11::~ScreenCapturerX11() { Destroy(); }
|
||||
|
||||
int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
|
||||
Destroy();
|
||||
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Cannot connect to X server");
|
||||
return -1;
|
||||
}
|
||||
|
||||
root_ = DefaultRootWindow(display_);
|
||||
screen_res_ = XRRGetScreenResources(display_, root_);
|
||||
if (!screen_res_) {
|
||||
LOG_ERROR("Failed to get screen resources");
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < screen_res_->noutput; ++i) {
|
||||
RROutput output = screen_res_->outputs[i];
|
||||
XRROutputInfo* output_info =
|
||||
XRRGetOutputInfo(display_, screen_res_, output);
|
||||
|
||||
if (output_info->connection == RR_Connected && output_info->crtc != 0) {
|
||||
XRRCrtcInfo* crtc_info =
|
||||
XRRGetCrtcInfo(display_, screen_res_, output_info->crtc);
|
||||
|
||||
std::string name(output_info->name);
|
||||
|
||||
if (name.empty()) {
|
||||
name = "Display" + std::to_string(i + 1);
|
||||
}
|
||||
|
||||
display_info_list_.push_back(DisplayInfo(
|
||||
(void*)display_, name, true, crtc_info->x, crtc_info->y,
|
||||
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height));
|
||||
|
||||
XRRFreeCrtcInfo(crtc_info);
|
||||
}
|
||||
|
||||
if (output_info) {
|
||||
XRRFreeOutputInfo(output_info);
|
||||
}
|
||||
}
|
||||
|
||||
XWindowAttributes attr;
|
||||
XGetWindowAttributes(display_, root_, &attr);
|
||||
|
||||
width_ = attr.width;
|
||||
height_ = attr.height;
|
||||
|
||||
if ((width_ & 1) != 0 || (height_ & 1) != 0) {
|
||||
LOG_WARN("X11 root size {}x{} is not even, aligning down to {}x{} for NV12",
|
||||
width_, height_, width_ & ~1, height_ & ~1);
|
||||
width_ &= ~1;
|
||||
height_ &= ~1;
|
||||
}
|
||||
|
||||
if (width_ <= 1 || height_ <= 1) {
|
||||
LOG_ERROR("Invalid capture size after alignment: {}x{}", width_, height_);
|
||||
return -2;
|
||||
}
|
||||
|
||||
fps_ = fps;
|
||||
callback_ = cb;
|
||||
|
||||
y_plane_.resize(width_ * height_);
|
||||
uv_plane_.resize((width_ / 2) * (height_ / 2) * 2);
|
||||
|
||||
if (!ProbeCapture()) {
|
||||
LOG_ERROR("X11 backend probe failed, XGetImage is not usable");
|
||||
return -3;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::Destroy() {
|
||||
Stop();
|
||||
|
||||
y_plane_.clear();
|
||||
uv_plane_.clear();
|
||||
|
||||
if (screen_res_) {
|
||||
XRRFreeScreenResources(screen_res_);
|
||||
screen_res_ = nullptr;
|
||||
}
|
||||
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::Start(bool show_cursor) {
|
||||
if (running_) return 0;
|
||||
show_cursor_ = show_cursor;
|
||||
running_ = true;
|
||||
paused_ = false;
|
||||
capture_error_count_ = 0;
|
||||
thread_ = std::thread([this]() {
|
||||
using clock = std::chrono::steady_clock;
|
||||
const auto frame_interval =
|
||||
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
|
||||
|
||||
while (running_) {
|
||||
const auto frame_start = clock::now();
|
||||
if (!paused_) {
|
||||
OnFrame();
|
||||
}
|
||||
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(clock::now() -
|
||||
frame_start);
|
||||
if (elapsed < frame_interval) {
|
||||
std::this_thread::sleep_for(frame_interval - elapsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::Stop() {
|
||||
if (!running_) return 0;
|
||||
running_ = false;
|
||||
if (thread_.joinable()) thread_.join();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::Pause(int monitor_index) {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::Resume(int monitor_index) {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::SwitchTo(int monitor_index) {
|
||||
monitor_index_ = monitor_index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::ResetToInitialMonitor() {
|
||||
monitor_index_ = initial_monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
void ScreenCapturerX11::OnFrame() {
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
const int monitor_index = monitor_index_.load();
|
||||
if (monitor_index < 0 ||
|
||||
monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid monitor index: {}", monitor_index);
|
||||
return;
|
||||
}
|
||||
|
||||
left_ = display_info_list_[monitor_index].left;
|
||||
top_ = display_info_list_[monitor_index].top;
|
||||
width_ = display_info_list_[monitor_index].width & ~1;
|
||||
height_ = display_info_list_[monitor_index].height & ~1;
|
||||
|
||||
if (width_ <= 1 || height_ <= 1) {
|
||||
LOG_ERROR("Invalid capture size: {}x{}", width_, height_);
|
||||
return;
|
||||
}
|
||||
|
||||
XImage* image = nullptr;
|
||||
int x11_error = 0;
|
||||
{
|
||||
ScopedX11ErrorTrap trap(display_);
|
||||
image = XGetImage(display_, root_, left_, top_, width_, height_, AllPlanes,
|
||||
ZPixmap);
|
||||
x11_error = trap.SyncAndGetError();
|
||||
}
|
||||
|
||||
if (x11_error != 0 || !image) {
|
||||
if (image) {
|
||||
XDestroyImage(image);
|
||||
}
|
||||
++capture_error_count_;
|
||||
if (capture_error_count_ == 1 || capture_error_count_ % 120 == 0) {
|
||||
LOG_WARN("X11 capture failed: x11_error={}, image={}, consecutive={}",
|
||||
x11_error, image ? "valid" : "null", capture_error_count_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
capture_error_count_ = 0;
|
||||
|
||||
// if enable show cursor, draw cursor
|
||||
if (show_cursor_) {
|
||||
Window root_return, child_return;
|
||||
int root_x, root_y, win_x, win_y;
|
||||
unsigned int mask;
|
||||
if (XQueryPointer(display_, root_, &root_return, &child_return, &root_x,
|
||||
&root_y, &win_x, &win_y, &mask)) {
|
||||
if (root_x >= left_ && root_x < left_ + width_ && root_y >= top_ &&
|
||||
root_y < top_ + height_) {
|
||||
DrawCursor(image, root_x - left_, root_y - top_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (image->bits_per_pixel != 32 || image->bytes_per_line <= 0) {
|
||||
LOG_WARN(
|
||||
"Unsupported X11 image layout: bits_per_pixel={}, bytes_per_line={}",
|
||||
image->bits_per_pixel, image->bytes_per_line);
|
||||
XDestroyImage(image);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* src_argb = reinterpret_cast<const uint8_t*>(image->data);
|
||||
const int src_stride_argb = image->bytes_per_line;
|
||||
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(width_) * static_cast<size_t>(height_);
|
||||
const size_t uv_size = y_size / 2;
|
||||
if (y_plane_.size() != y_size) {
|
||||
y_plane_.resize(y_size);
|
||||
}
|
||||
if (uv_plane_.size() != uv_size) {
|
||||
uv_plane_.resize(uv_size);
|
||||
}
|
||||
|
||||
const int convert_ret =
|
||||
use_abgr_to_nv12_
|
||||
? libyuv::ABGRToNV12(src_argb, src_stride_argb, y_plane_.data(),
|
||||
width_, uv_plane_.data(), width_, width_,
|
||||
height_)
|
||||
: libyuv::ARGBToNV12(src_argb, src_stride_argb, y_plane_.data(),
|
||||
width_, uv_plane_.data(), width_, width_,
|
||||
height_);
|
||||
if (convert_ret != 0) {
|
||||
LOG_WARN("X11 {} failed: {}",
|
||||
use_abgr_to_nv12_ ? "ABGRToNV12" : "ARGBToNV12", convert_ret);
|
||||
XDestroyImage(image);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> nv12;
|
||||
nv12.reserve(y_plane_.size() + uv_plane_.size());
|
||||
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
|
||||
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
|
||||
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(monitor_index);
|
||||
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
|
||||
stream_id.c_str());
|
||||
}
|
||||
|
||||
XDestroyImage(image);
|
||||
}
|
||||
|
||||
void ScreenCapturerX11::DrawCursor(XImage* image, int x, int y) {
|
||||
if (!display_ || !image) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check XFixes extension
|
||||
int event_base, error_base;
|
||||
if (!XFixesQueryExtension(display_, &event_base, &error_base)) {
|
||||
return;
|
||||
}
|
||||
|
||||
XFixesCursorImage* cursor_image = XFixesGetCursorImage(display_);
|
||||
if (!cursor_image) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cursor_width = cursor_image->width;
|
||||
int cursor_height = cursor_image->height;
|
||||
|
||||
int draw_x = x - cursor_image->xhot;
|
||||
int draw_y = y - cursor_image->yhot;
|
||||
|
||||
// draw cursor on image
|
||||
for (int cy = 0; cy < cursor_height; ++cy) {
|
||||
for (int cx = 0; cx < cursor_width; ++cx) {
|
||||
int img_x = draw_x + cx;
|
||||
int img_y = draw_y + cy;
|
||||
|
||||
if (img_x < 0 || img_x >= image->width || img_y < 0 ||
|
||||
img_y >= image->height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned long cursor_pixel = cursor_image->pixels[cy * cursor_width + cx];
|
||||
unsigned char a = (cursor_pixel >> 24) & 0xFF;
|
||||
|
||||
// if alpha is 0, skip
|
||||
if (a == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned long img_pixel = XGetPixel(image, img_x, img_y);
|
||||
|
||||
unsigned char img_r = (img_pixel >> 16) & 0xFF;
|
||||
unsigned char img_g = (img_pixel >> 8) & 0xFF;
|
||||
unsigned char img_b = img_pixel & 0xFF;
|
||||
|
||||
unsigned char cursor_r = (cursor_pixel >> 16) & 0xFF;
|
||||
unsigned char cursor_g = (cursor_pixel >> 8) & 0xFF;
|
||||
unsigned char cursor_b = cursor_pixel & 0xFF;
|
||||
|
||||
// alpha mix
|
||||
unsigned char final_r, final_g, final_b;
|
||||
if (a == 255) {
|
||||
// if alpha is 255, use cursor color
|
||||
final_r = cursor_r;
|
||||
final_g = cursor_g;
|
||||
final_b = cursor_b;
|
||||
} else {
|
||||
float alpha = a / 255.0f;
|
||||
float inv_alpha = 1.0f - alpha;
|
||||
final_r =
|
||||
static_cast<unsigned char>(cursor_r * alpha + img_r * inv_alpha);
|
||||
final_g =
|
||||
static_cast<unsigned char>(cursor_g * alpha + img_g * inv_alpha);
|
||||
final_b =
|
||||
static_cast<unsigned char>(cursor_b * alpha + img_b * inv_alpha);
|
||||
}
|
||||
|
||||
// set pixel
|
||||
unsigned long new_pixel = (final_r << 16) | (final_g << 8) | final_b;
|
||||
XPutPixel(image, img_x, img_y, new_pixel);
|
||||
}
|
||||
}
|
||||
|
||||
XFree(cursor_image);
|
||||
}
|
||||
|
||||
bool ScreenCapturerX11::ProbeCapture() {
|
||||
if (!display_ || display_info_list_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& first_display = display_info_list_[0];
|
||||
XImage* probe_image = nullptr;
|
||||
int x11_error = 0;
|
||||
{
|
||||
ScopedX11ErrorTrap trap(display_);
|
||||
probe_image = XGetImage(display_, root_, first_display.left,
|
||||
first_display.top, 1, 1, AllPlanes, ZPixmap);
|
||||
x11_error = trap.SyncAndGetError();
|
||||
}
|
||||
|
||||
if (x11_error != 0 || !probe_image) {
|
||||
LOG_WARN("X11 probe XGetImage failed: x11_error={}, image={}", x11_error,
|
||||
probe_image ? "valid" : "null");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool red_in_low_byte = (probe_image->red_mask & 0x000000FFu) != 0;
|
||||
const bool blue_in_low_byte = (probe_image->blue_mask & 0x000000FFu) != 0;
|
||||
use_abgr_to_nv12_ = red_in_low_byte && !blue_in_low_byte;
|
||||
|
||||
XDestroyImage(probe_image);
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-05-07
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_X11_H_
|
||||
#define _SCREEN_CAPTURER_X11_H_
|
||||
|
||||
// forward declarations for X11 types
|
||||
struct _XDisplay;
|
||||
typedef struct _XDisplay Display;
|
||||
typedef unsigned long Window;
|
||||
struct _XRRScreenResources;
|
||||
typedef struct _XRRScreenResources XRRScreenResources;
|
||||
struct _XImage;
|
||||
typedef struct _XImage XImage;
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerX11 : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerX11();
|
||||
~ScreenCapturerX11();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
void OnFrame();
|
||||
|
||||
private:
|
||||
void DrawCursor(XImage* image, int x, int y);
|
||||
bool ProbeCapture();
|
||||
|
||||
private:
|
||||
Display* display_ = nullptr;
|
||||
Window root_ = 0;
|
||||
XRRScreenResources* screen_res_ = nullptr;
|
||||
int left_ = 0;
|
||||
int top_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
int capture_error_count_ = 0;
|
||||
bool use_abgr_to_nv12_ = false;
|
||||
|
||||
std::vector<uint8_t> y_plane_;
|
||||
std::vector<uint8_t> uv_plane_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "speaker_capturer_factory.h"
|
||||
|
||||
#include "speaker_capturer_linux.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
SpeakerCapturer* SpeakerCapturerFactory::Create() {
|
||||
return new SpeakerCapturerLinux();
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,271 @@
|
||||
#include "speaker_capturer_linux.h"
|
||||
|
||||
#include <pulse/error.h>
|
||||
#include <pulse/introspect.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
constexpr int kSampleRate = 48000;
|
||||
constexpr pa_sample_format_t kFormat = PA_SAMPLE_S16LE;
|
||||
constexpr int kChannels = 1;
|
||||
constexpr size_t kFrameSizeBytes = 480 * sizeof(int16_t);
|
||||
|
||||
SpeakerCapturerLinux::SpeakerCapturerLinux()
|
||||
: inited_(false), paused_(false), stop_flag_(false) {}
|
||||
SpeakerCapturerLinux::~SpeakerCapturerLinux() {
|
||||
Stop();
|
||||
Destroy();
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Init(speaker_data_cb cb) {
|
||||
if (inited_) return 0;
|
||||
cb_ = cb;
|
||||
inited_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Destroy() {
|
||||
inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string SpeakerCapturerLinux::GetDefaultMonitorSourceName() {
|
||||
std::string monitor_name;
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
bool ready = false;
|
||||
|
||||
pa_mainloop* mainloop = pa_mainloop_new();
|
||||
pa_mainloop_api* api = pa_mainloop_get_api(mainloop);
|
||||
pa_context* context = pa_context_new(api, "GetMonitor");
|
||||
|
||||
struct CallbackState {
|
||||
std::string* name;
|
||||
std::mutex* mtx;
|
||||
std::condition_variable* cv;
|
||||
bool* ready;
|
||||
} state{&monitor_name, &mtx, &cv, &ready};
|
||||
|
||||
pa_context_set_state_callback(
|
||||
context,
|
||||
[](pa_context* c, void* userdata) {
|
||||
auto* state = static_cast<CallbackState*>(userdata);
|
||||
if (pa_context_get_state(c) == PA_CONTEXT_READY) {
|
||||
pa_operation* operation = pa_context_get_server_info(
|
||||
c,
|
||||
[](pa_context*, const pa_server_info* info, void* userdata) {
|
||||
auto* state = static_cast<CallbackState*>(userdata);
|
||||
if (info && info->default_sink_name) {
|
||||
*(state->name) =
|
||||
std::string(info->default_sink_name) + ".monitor";
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*(state->mtx));
|
||||
*(state->ready) = true;
|
||||
}
|
||||
state->cv->notify_one();
|
||||
},
|
||||
userdata);
|
||||
if (operation) {
|
||||
pa_operation_unref(operation);
|
||||
}
|
||||
}
|
||||
},
|
||||
&state);
|
||||
|
||||
pa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr);
|
||||
|
||||
std::thread loop_thread([&]() {
|
||||
int ret = 0;
|
||||
pa_mainloop_run(mainloop, &ret);
|
||||
});
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
cv.wait_for(lock, std::chrono::seconds(2), [&] { return ready; });
|
||||
}
|
||||
|
||||
pa_context_disconnect(context);
|
||||
pa_context_unref(context);
|
||||
pa_mainloop_quit(mainloop, 0);
|
||||
loop_thread.join();
|
||||
pa_mainloop_free(mainloop);
|
||||
|
||||
return monitor_name;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Start() {
|
||||
if (!inited_ || mainloop_thread_.joinable()) return -1;
|
||||
stop_flag_ = false;
|
||||
|
||||
mainloop_thread_ = std::thread([this]() {
|
||||
std::string monitor_name = GetDefaultMonitorSourceName();
|
||||
if (monitor_name.empty()) {
|
||||
LOG_ERROR("Failed to get monitor source");
|
||||
return;
|
||||
}
|
||||
|
||||
mainloop_ = pa_threaded_mainloop_new();
|
||||
pa_mainloop_api* api = pa_threaded_mainloop_get_api(mainloop_);
|
||||
context_ = pa_context_new(api, "SpeakerCapturer");
|
||||
|
||||
pa_context_set_state_callback(
|
||||
context_,
|
||||
[](pa_context* c, void* userdata) {
|
||||
auto self = static_cast<SpeakerCapturerLinux*>(userdata);
|
||||
pa_context_state_t state = pa_context_get_state(c);
|
||||
if (state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED ||
|
||||
state == PA_CONTEXT_TERMINATED) {
|
||||
pa_threaded_mainloop_signal(self->mainloop_, 0);
|
||||
}
|
||||
},
|
||||
this);
|
||||
|
||||
if (pa_threaded_mainloop_start(mainloop_) < 0) {
|
||||
LOG_ERROR("Failed to start mainloop");
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
|
||||
if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFLAGS, nullptr) <
|
||||
0) {
|
||||
LOG_ERROR("Failed to connect context");
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pa_context_state_t state = pa_context_get_state(context_);
|
||||
if (state == PA_CONTEXT_READY) break;
|
||||
if (!PA_CONTEXT_IS_GOOD(state) || stop_flag_) {
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
pa_threaded_mainloop_wait(mainloop_);
|
||||
}
|
||||
|
||||
pa_sample_spec ss = {kFormat, kSampleRate, kChannels};
|
||||
stream_ = pa_stream_new(context_, "Capture", &ss, nullptr);
|
||||
|
||||
pa_stream_set_read_callback(
|
||||
stream_,
|
||||
[](pa_stream* s, size_t len, void* u) {
|
||||
auto self = static_cast<SpeakerCapturerLinux*>(u);
|
||||
if (self->paused_ || self->stop_flag_) return;
|
||||
|
||||
const void* data = nullptr;
|
||||
if (pa_stream_peek(s, &data, &len) < 0 || !data) return;
|
||||
|
||||
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||
self->frame_cache_.insert(self->frame_cache_.end(), p, p + len);
|
||||
|
||||
while (self->frame_cache_.size() >= kFrameSizeBytes) {
|
||||
std::vector<uint8_t> temp_frame(
|
||||
self->frame_cache_.begin(),
|
||||
self->frame_cache_.begin() + kFrameSizeBytes);
|
||||
self->cb_(temp_frame.data(), kFrameSizeBytes, "audio");
|
||||
self->frame_cache_.erase(
|
||||
self->frame_cache_.begin(),
|
||||
self->frame_cache_.begin() + kFrameSizeBytes);
|
||||
}
|
||||
|
||||
pa_stream_drop(s);
|
||||
},
|
||||
this);
|
||||
|
||||
pa_buffer_attr attr = {.maxlength = (uint32_t)-1,
|
||||
.tlength = 0,
|
||||
.prebuf = 0,
|
||||
.minreq = 0,
|
||||
.fragsize = (uint32_t)kFrameSizeBytes};
|
||||
|
||||
if (pa_stream_connect_record(stream_, monitor_name.c_str(), &attr,
|
||||
PA_STREAM_ADJUST_LATENCY) < 0) {
|
||||
LOG_ERROR("Failed to connect stream");
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pa_stream_state_t s = pa_stream_get_state(stream_);
|
||||
if (s == PA_STREAM_READY) break;
|
||||
if (!PA_STREAM_IS_GOOD(s) || stop_flag_) {
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
pa_threaded_mainloop_wait(mainloop_);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
|
||||
while (!stop_flag_)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Stop() {
|
||||
stop_flag_ = true;
|
||||
|
||||
if (mainloop_) {
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_threaded_mainloop_signal(mainloop_, 0);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
|
||||
if (mainloop_thread_.joinable()) {
|
||||
mainloop_thread_.join();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SpeakerCapturerLinux::Cleanup() {
|
||||
if (mainloop_) {
|
||||
pa_threaded_mainloop_stop(mainloop_);
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
|
||||
if (stream_) {
|
||||
pa_stream_disconnect(stream_);
|
||||
pa_stream_unref(stream_);
|
||||
stream_ = nullptr;
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
pa_context_disconnect(context_);
|
||||
pa_context_unref(context_);
|
||||
context_ = nullptr;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
pa_threaded_mainloop_free(mainloop_);
|
||||
mainloop_ = nullptr;
|
||||
}
|
||||
|
||||
frame_cache_.clear();
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Pause() {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Resume() {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-07-15
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SPEAKER_CAPTURER_LINUX_H_
|
||||
#define _SPEAKER_CAPTURER_LINUX_H_
|
||||
|
||||
#include <pulse/pulseaudio.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "speaker_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class SpeakerCapturerLinux : public SpeakerCapturer {
|
||||
public:
|
||||
SpeakerCapturerLinux();
|
||||
~SpeakerCapturerLinux();
|
||||
|
||||
int Init(speaker_data_cb cb) override;
|
||||
int Destroy() override;
|
||||
int Start() override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause();
|
||||
int Resume();
|
||||
|
||||
private:
|
||||
std::string GetDefaultMonitorSourceName();
|
||||
void Cleanup();
|
||||
|
||||
private:
|
||||
speaker_data_cb cb_ = nullptr;
|
||||
|
||||
std::atomic<bool> inited_;
|
||||
std::atomic<bool> paused_;
|
||||
std::atomic<bool> stop_flag_;
|
||||
|
||||
std::thread mainloop_thread_;
|
||||
pa_threaded_mainloop* mainloop_ = nullptr;
|
||||
pa_context* context_ = nullptr;
|
||||
pa_stream* stream_ = nullptr;
|
||||
|
||||
std::mutex state_mtx_;
|
||||
std::vector<uint8_t> frame_cache_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string GetMac() {
|
||||
const int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (socket_fd < 0) return {};
|
||||
|
||||
char buffer[1024] = {};
|
||||
struct ifconf config = {};
|
||||
config.ifc_len = sizeof(buffer);
|
||||
config.ifc_buf = buffer;
|
||||
if (ioctl(socket_fd, SIOCGIFCONF, &config) < 0) {
|
||||
close(socket_fd);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result;
|
||||
struct ifreq request = {};
|
||||
struct ifreq* cursor = config.ifc_req;
|
||||
const struct ifreq* end = cursor + config.ifc_len / sizeof(struct ifreq);
|
||||
for (; cursor != end; ++cursor) {
|
||||
std::strncpy(request.ifr_name, cursor->ifr_name,
|
||||
sizeof(request.ifr_name) - 1);
|
||||
if (ioctl(socket_fd, SIOCGIFFLAGS, &request) < 0 ||
|
||||
(request.ifr_flags & IFF_LOOPBACK) ||
|
||||
ioctl(socket_fd, SIOCGIFHWADDR, &request) < 0) {
|
||||
continue;
|
||||
}
|
||||
char address[16] = {};
|
||||
int length = 0;
|
||||
for (int index = 0; index < 6; ++index) {
|
||||
length += std::snprintf(
|
||||
address + length, sizeof(address) - length, "%.2X",
|
||||
static_cast<unsigned char>(request.ifr_hwaddr.sa_data[index]));
|
||||
}
|
||||
result = address;
|
||||
break;
|
||||
}
|
||||
close(socket_fd);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string GetHostName() {
|
||||
char hostname[256] = {};
|
||||
if (gethostname(hostname, sizeof(hostname)) == -1) {
|
||||
LOG_ERROR("gethostname failed");
|
||||
return {};
|
||||
}
|
||||
return hostname;
|
||||
}
|
||||
|
||||
bool IsWaylandSession() {
|
||||
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* display = std::getenv("WAYLAND_DISPLAY");
|
||||
return display && display[0] != '\0';
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
Reference in New Issue
Block a user