From 710918a622824d6970886bec027dd04310497cc0 Mon Sep 17 00:00:00 2001 From: "Tobias J. Endres" Date: Thu, 14 Aug 2025 04:26:53 +0200 Subject: [PATCH] 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). --- CMakeLists.txt | 34 ++++--- wayland_poc.cpp | 255 ++++++++---------------------------------------- 2 files changed, 62 insertions(+), 227 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bb8005a..2ea9eaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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} ) diff --git a/wayland_poc.cpp b/wayland_poc.cpp index 7d763f6..e67ddeb 100644 --- a/wayland_poc.cpp +++ b/wayland_poc.cpp @@ -1,253 +1,76 @@ #include #include -#include -#include -#include +#include +#include +#include +#include +#include -// PipeWire includes (will need to be installed on the system) -#include -#include -#include -#include -#include +// 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 // 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 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()) { + qCritical() << "xdg-desktop-portal response first argument is not a map."; + QCoreApplication::quit(); return; } - QImage image(reinterpret_cast(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(); -} +} \ No newline at end of file