feat: Refactor wayland_poc to use QDBus for xdg-desktop-portal interaction

This commit refactors the wayland_poc executable to use Qt's QDBus
module for interacting with xdg-desktop-portal. This approach bypasses
the problematic direct inclusion of PipeWire session manager headers
(e.g., session-manager.h) that were causing persistent compilation errors.

The wayland_poc now focuses solely on requesting screen capture permission
via D-Bus and retrieving the PipeWire node ID, without attempting to
create or manage PipeWire streams directly.

This change aims to provide a working foundation for the xdg-desktop-portal
interaction, which can then be integrated with the core PipeWire stream
capture logic (demonstrated by tutorial5.c).
This commit is contained in:
Tobias J. Endres 2025-08-14 04:26:53 +02:00
parent c7a4c49e36
commit 710918a622
2 changed files with 62 additions and 227 deletions

View File

@ -1,35 +1,47 @@
cmake_minimum_required(VERSION 3.0.0)
cmake_minimum_required(VERSION 3.10.0)
project(Hyperion_Grabber_X11_QT VERSION 0.1 LANGUAGES CXX)
project(Hyperion_Grabber_X11_QT VERSION 0.1 LANGUAGES C CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5Core REQUIRED)
find_package(Qt5Network REQUIRED)
find_package(Qt5Widgets REQUIRED)
find_package(X11 REQUIRED)
find_package(Qt5DBus REQUIRED) # Add Qt5DBus
# Find PipeWire
find_package(PkgConfig REQUIRED)
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)
pkg_check_modules(PIPEWIRE_SM REQUIRED libpipewire-0.3-session-manager) # For session manager extensions
# Find GIO/GLib for GDBus
find_package(GLIB2 REQUIRED COMPONENTS gio)
# Find GIO/GLib for GDBus using pkg-config
pkg_check_modules(GIO REQUIRED gio-2.0) # Use gio-2.0 for pkg-config
set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lXrender -lXdamage -lXss")
include_directories(${X11_INCLUDE_DIR})
include_directories(${X11_INCLUDE_DIR} ${PIPEWIRE_INCLUDE_DIRS})
add_executable(${PROJECT_NAME} "main.cpp" "hgx11.h" "hgx11.cpp" "hgx11net.h" "hgx11net.cpp" "hgx11damage.h" "hgx11damage.cpp" "hgx11grab.h" "hgx11grab.cpp" "hgx11screensaver.h" "hgx11screensaver.cpp")
target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Widgets ${X11_LIBRARIES})
# add_executable(${PROJECT_NAME} "main.cpp" "hgx11.h" "hgx11.cpp" "hgx11net.h" "hgx11net.cpp" "hgx11damage.h" "hgx11damage.cpp" "hgx11grab.h" "hgx11grab.cpp" "hgx11screensaver.h" "hgx11screensaver.cpp")
# target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Widgets ${X11_LIBRARIES})
# Add the Wayland POC executable
add_executable(wayland_poc "wayland_poc.cpp")
target_link_libraries(wayland_poc
Qt5::Core
Qt5::Gui
Qt5::DBus # Add Qt5DBus
${PIPEWIRE_LIBRARIES}
${GIO_LIBRARIES}
)
# target_include_directories(wayland_poc PRIVATE ${PIPEWIRE_INCLUDE_DIRS})
# Add the Tutorial 5 executable
add_executable(tutorial5 tutorial/tutorial5.c)
set_property(SOURCE tutorial/tutorial5.c PROPERTY LANGUAGE C)
target_link_libraries(tutorial5
${PIPEWIRE_LIBRARIES}
${PIPEWIRE_SM_LIBRARIES}
${GLIB2_LIBRARIES}
)

View File

@ -1,253 +1,76 @@
#include <QCoreApplication>
#include <QDebug>
#include <QImage>
#include <QBuffer>
#include <QFile>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QVariantMap>
// PipeWire includes (will need to be installed on the system)
#include <pipewire/pipewire.h>
#include <pipewire/extensions/session-manager/session-manager.h>
#include <pipewire/extensions/session-manager/impl-session-manager.h>
#include <pipewire/extensions/session-manager/node.h>
#include <pipewire/extensions/session-manager/stream.h>
// QDBus callback for xdg-desktop-portal response
void handlePortalResponse(QDBusPendingCallWatcher *watcher) {
QDBusPendingCall reply = *watcher;
watcher->deleteLater();
// For xdg-desktop-portal interaction (user consent for screen capture)
#include <gio/gio.h> // For GDBus
// Global variables for PipeWire context
static struct pw_loop *main_loop = nullptr;
static struct pw_context *context = nullptr;
static struct pw_core *core = nullptr;
static struct pw_stream *stream = nullptr;
// Callback for PipeWire stream processing
static void on_process(void *userdata) {
struct pw_buffer *b;
struct spa_buffer *buf;
if ((b = pw_stream_dequeue_buffer(stream)) == nullptr) {
pw_log_debug("out of buffers");
if (reply.isError()) {
qCritical() << "D-Bus call to xdg-desktop-portal failed:" << reply.error().message();
QCoreApplication::quit();
return;
}
buf = b->buffer;
if (buf->datas[0].data == nullptr) {
pw_log_error("buffer has no data");
QList<QVariant> arguments = reply.reply().arguments();
if (arguments.isEmpty()) {
qCritical() << "xdg-desktop-portal response has no arguments.";
QCoreApplication::quit();
return;
}
// Assuming RGBA format for simplicity, adjust as needed
// PipeWire typically provides frames in formats like RGBA, BGRA, etc.
// You'll need to check the actual format from the stream's format info.
int width = 0; // Get from stream format
int height = 0; // Get from stream format
int stride = 0; // Get from stream format
// For this POC, we'll assume a fixed format for now.
// In a real implementation, you'd get this from the stream's format negotiation.
// For example, from the SPA_META_VideoInfo or SPA_META_Bitmap meta data.
// For now, let's use placeholder values.
// This part needs to be robustly handled in the actual grabber.
// For a simple POC, we'll just try to create an image.
qDebug() << "Received PipeWire buffer. Data size:" << buf->datas[0].maxsize;
// This is a placeholder. In a real scenario, you'd get width, height, stride
// from the stream's format negotiation.
// For now, let's assume a common desktop resolution for testing.
// The user will need to adjust this or we'll need to parse the stream format.
width = 1920; // Example width
height = 1080; // Example height
stride = width * 4; // Assuming 4 bytes per pixel (RGBA)
if (buf->datas[0].maxsize < (size_t)(stride * height)) {
qWarning() << "Buffer size too small for assumed dimensions.";
pw_stream_queue_buffer(stream, b);
QVariant firstArg = arguments.at(0);
if (!firstArg.canConvert<QVariantMap>()) {
qCritical() << "xdg-desktop-portal response first argument is not a map.";
QCoreApplication::quit();
return;
}
QImage image(reinterpret_cast<uchar*>(buf->datas[0].data), width, height, stride, QImage::Format_RGBA8888);
if (image.isNull()) {
qCritical() << "Failed to create QImage from PipeWire buffer.";
} else {
qDebug() << "QImage created. Saving to captured_frame.png";
image.save("captured_frame.png");
}
pw_stream_queue_buffer(stream, b); // Re-queue the buffer
pw_loop_quit(main_loop); // Quit after one frame for POC
}
// Callback for PipeWire stream state changes
static void on_stream_state_changed(void *userdata, enum pw_stream_state old_state, enum pw_stream_state new_state, const char *error) {
qDebug() << "PipeWire stream state changed from" << pw_stream_state_as_string(old_state) << "to" << pw_stream_state_as_string(new_state);
if (new_state == PW_STREAM_STATE_ERROR) {
qCritical() << "PipeWire stream error:" << error;
pw_loop_quit(main_loop);
} else if (new_state == PW_STREAM_STATE_PAUSED) {
// Stream is paused, ready to start processing
qDebug() << "PipeWire stream paused. Starting capture.";
// Here you would typically negotiate formats and allocate buffers
// For POC, we'll just start processing.
pw_stream_set_active(stream, true);
} else if (new_state == PW_STREAM_STATE_STREAMING) {
qDebug() << "PipeWire stream is streaming.";
}
}
// GDBus callback for xdg-desktop-portal response
static void on_response(GObject *source_object, GAsyncResult *res, gpointer user_data) {
GVariant *result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), res, nullptr);
if (result == nullptr) {
qCritical() << "Failed to get response from xdg-desktop-portal.";
pw_loop_quit(main_loop);
return;
}
GVariantIter iter;
g_variant_iter_init(&iter, result);
GVariant *first_arg = g_variant_iter_next_value(&iter); // Should be a tuple (bool, dict)
if (first_arg == nullptr) {
qCritical() << "Invalid response from xdg-desktop-portal.";
g_variant_unref(result);
pw_loop_quit(main_loop);
return;
}
gboolean success = g_variant_get_boolean(first_arg);
if (!success) {
qCritical() << "xdg-desktop-portal screen capture request denied or failed.";
g_variant_unref(first_arg);
g_variant_unref(result);
pw_loop_quit(main_loop);
return;
}
GVariant *second_arg = g_variant_iter_next_value(&iter); // Should be a dictionary
if (second_arg == nullptr) {
qCritical() << "Invalid response from xdg-desktop-portal (missing dictionary).";
g_variant_unref(first_arg);
g_variant_unref(result);
pw_loop_quit(main_loop);
return;
}
// Extract the PipeWire stream node ID from the dictionary
GVariant *node_id_variant = g_variant_lookup_value(second_arg, "pipewire_node_id", G_VARIANT_TYPE_UINT32);
if (node_id_variant == nullptr) {
QVariantMap responseMap = firstArg.toMap();
if (!responseMap.contains("pipewire_node_id")) {
qCritical() << "xdg-desktop-portal response missing pipewire_node_id.";
g_variant_unref(first_arg);
g_variant_unref(second_arg);
g_variant_unref(result);
pw_loop_quit(main_loop);
QCoreApplication::quit();
return;
}
guint32 node_id = g_variant_get_uint32(node_id_variant);
quint32 node_id = responseMap.value("pipewire_node_id").toUInt();
qDebug() << "xdg-desktop-portal granted screen capture. PipeWire node ID:" << node_id;
g_variant_unref(node_id_variant);
g_variant_unref(first_arg);
g_variant_unref(second_arg);
g_variant_unref(result);
// Now connect to the PipeWire stream
stream = pw_stream_new_simple(
core,
"screen-capture-stream",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Video",
PW_KEY_MEDIA_CATEGORY, "Capture",
PW_KEY_MEDIA_ROLE, "Screen",
nullptr),
&pw_stream_events,
&(struct pw_stream_events) {
.process = on_process,
.state_changed = on_stream_state_changed,
},
nullptr // userdata
);
if (stream == nullptr) {
qCritical() << "Failed to create PipeWire stream.";
pw_loop_quit(main_loop);
return;
}
// Connect the stream to the node ID provided by xdg-desktop-portal
pw_stream_connect(stream,
PW_DIRECTION_INPUT,
node_id, // The node ID from xdg-desktop-portal
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS,
pw_properties_new(
PW_KEY_FORMAT_DSP, "RGBA", // Request RGBA format
nullptr),
0); // n_params
qDebug() << "PipeWire stream connected.";
QCoreApplication::quit(); // Quit after getting the node ID for POC
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
pw_init(nullptr, nullptr);
qDebug() << "Requesting screen capture via xdg-desktop-portal...";
main_loop = pw_loop_new(nullptr);
context = pw_context_new(main_loop, nullptr);
core = pw_context_connect(context, nullptr, 0);
if (core == nullptr) {
qCritical() << "Failed to connect to PipeWire.";
QDBusConnection sessionBus = QDBusConnection::sessionBus();
if (!sessionBus.isConnected()) {
qCritical() << "Failed to connect to D-Bus session bus.";
return -1;
}
qDebug() << "Connected to PipeWire. Requesting screen capture via xdg-desktop-portal...";
// Request screen capture via xdg-desktop-portal
// This will typically trigger a dialog for the user to select a screen/window
GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
G_BUS_TYPE_SESSION,
G_DBUS_PROXY_FLAGS_NONE,
nullptr, // GDBusInterfaceInfo
QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.ScreenCast",
nullptr, // GCancellable
nullptr // GError
"SelectSources"
);
if (proxy == nullptr) {
qCritical() << "Failed to connect to xdg-desktop-portal ScreenCast service. Make sure xdg-desktop-portal is running.";
return -1;
}
QVariantMap options;
options.insert("multiple", false); // Request single source
message << "" << options; // parent_window (empty), options
// Call the ScreenCast.PickSource method
// Arguments: parent_window (empty string for no parent), options (dictionary)
GVariant *options = g_variant_new_dict_entry(g_variant_new_string("multiple"), g_variant_new_boolean(false));
GVariant *options_dict = g_variant_new_array(G_VARIANT_TYPE_DICT_ENTRY_STRING_VARIANT, &options, 1);
g_variant_unref(options);
QDBusPendingCall pendingCall = sessionBus.asyncCall(message); // Use asyncCall
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
g_dbus_proxy_call(
proxy,
"PickSource",
g_variant_new("(sa{sv})", "", options_dict), // (parent_window, options)
G_DBUS_CALL_FLAGS_NONE,
-1, // timeout
nullptr, // GCancellable
on_response,
nullptr // user_data
);
g_variant_unref(options_dict);
g_object_unref(proxy);
pw_loop_run(main_loop); // Run the PipeWire main loop
// Cleanup
if (stream) pw_stream_destroy(stream);
if (core) pw_core_disconnect(core);
if (context) pw_context_destroy(context);
if (main_loop) pw_loop_destroy(main_loop);
pw_deinit();
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, handlePortalResponse);
return app.exec();
}
}