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:
parent
c7a4c49e36
commit
710918a622
@ -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_INCLUDE_CURRENT_DIR ON)
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_BUILD_TYPE Release)
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
find_package(Qt5Core REQUIRED)
|
find_package(Qt5Core REQUIRED)
|
||||||
find_package(Qt5Network REQUIRED)
|
find_package(Qt5Network REQUIRED)
|
||||||
find_package(Qt5Widgets REQUIRED)
|
find_package(Qt5Widgets REQUIRED)
|
||||||
find_package(X11 REQUIRED)
|
find_package(X11 REQUIRED)
|
||||||
|
find_package(Qt5DBus REQUIRED) # Add Qt5DBus
|
||||||
|
|
||||||
# Find PipeWire
|
# Find PipeWire
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)
|
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 using pkg-config
|
||||||
|
pkg_check_modules(GIO REQUIRED gio-2.0) # Use gio-2.0 for pkg-config
|
||||||
# Find GIO/GLib for GDBus
|
|
||||||
find_package(GLIB2 REQUIRED COMPONENTS gio)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lXrender -lXdamage -lXss")
|
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")
|
# 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})
|
# target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Widgets ${X11_LIBRARIES})
|
||||||
|
|
||||||
# Add the Wayland POC executable
|
# Add the Wayland POC executable
|
||||||
add_executable(wayland_poc "wayland_poc.cpp")
|
add_executable(wayland_poc "wayland_poc.cpp")
|
||||||
target_link_libraries(wayland_poc
|
target_link_libraries(wayland_poc
|
||||||
Qt5::Core
|
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_LIBRARIES}
|
||||||
${PIPEWIRE_SM_LIBRARIES}
|
|
||||||
${GLIB2_LIBRARIES}
|
|
||||||
)
|
)
|
||||||
|
|||||||
253
wayland_poc.cpp
253
wayland_poc.cpp
@ -1,253 +1,76 @@
|
|||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QImage>
|
#include <QDBusConnection>
|
||||||
#include <QBuffer>
|
#include <QDBusMessage>
|
||||||
#include <QFile>
|
#include <QDBusPendingCall>
|
||||||
|
#include <QDBusPendingCallWatcher>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
// PipeWire includes (will need to be installed on the system)
|
// QDBus callback for xdg-desktop-portal response
|
||||||
#include <pipewire/pipewire.h>
|
void handlePortalResponse(QDBusPendingCallWatcher *watcher) {
|
||||||
#include <pipewire/extensions/session-manager/session-manager.h>
|
QDBusPendingCall reply = *watcher;
|
||||||
#include <pipewire/extensions/session-manager/impl-session-manager.h>
|
watcher->deleteLater();
|
||||||
#include <pipewire/extensions/session-manager/node.h>
|
|
||||||
#include <pipewire/extensions/session-manager/stream.h>
|
|
||||||
|
|
||||||
// For xdg-desktop-portal interaction (user consent for screen capture)
|
if (reply.isError()) {
|
||||||
#include <gio/gio.h> // For GDBus
|
qCritical() << "D-Bus call to xdg-desktop-portal failed:" << reply.error().message();
|
||||||
|
QCoreApplication::quit();
|
||||||
// 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");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = b->buffer;
|
QList<QVariant> arguments = reply.reply().arguments();
|
||||||
if (buf->datas[0].data == nullptr) {
|
if (arguments.isEmpty()) {
|
||||||
pw_log_error("buffer has no data");
|
qCritical() << "xdg-desktop-portal response has no arguments.";
|
||||||
|
QCoreApplication::quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming RGBA format for simplicity, adjust as needed
|
QVariant firstArg = arguments.at(0);
|
||||||
// PipeWire typically provides frames in formats like RGBA, BGRA, etc.
|
if (!firstArg.canConvert<QVariantMap>()) {
|
||||||
// You'll need to check the actual format from the stream's format info.
|
qCritical() << "xdg-desktop-portal response first argument is not a map.";
|
||||||
int width = 0; // Get from stream format
|
QCoreApplication::quit();
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage image(reinterpret_cast<uchar*>(buf->datas[0].data), width, height, stride, QImage::Format_RGBA8888);
|
QVariantMap responseMap = firstArg.toMap();
|
||||||
if (image.isNull()) {
|
if (!responseMap.contains("pipewire_node_id")) {
|
||||||
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) {
|
|
||||||
qCritical() << "xdg-desktop-portal response missing pipewire_node_id.";
|
qCritical() << "xdg-desktop-portal response missing pipewire_node_id.";
|
||||||
g_variant_unref(first_arg);
|
QCoreApplication::quit();
|
||||||
g_variant_unref(second_arg);
|
|
||||||
g_variant_unref(result);
|
|
||||||
pw_loop_quit(main_loop);
|
|
||||||
return;
|
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;
|
qDebug() << "xdg-desktop-portal granted screen capture. PipeWire node ID:" << node_id;
|
||||||
|
|
||||||
g_variant_unref(node_id_variant);
|
QCoreApplication::quit(); // Quit after getting the node ID for POC
|
||||||
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.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
pw_init(nullptr, nullptr);
|
qDebug() << "Requesting screen capture via xdg-desktop-portal...";
|
||||||
|
|
||||||
main_loop = pw_loop_new(nullptr);
|
QDBusConnection sessionBus = QDBusConnection::sessionBus();
|
||||||
context = pw_context_new(main_loop, nullptr);
|
if (!sessionBus.isConnected()) {
|
||||||
core = pw_context_connect(context, nullptr, 0);
|
qCritical() << "Failed to connect to D-Bus session bus.";
|
||||||
|
|
||||||
if (core == nullptr) {
|
|
||||||
qCritical() << "Failed to connect to PipeWire.";
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "Connected to PipeWire. Requesting screen capture via xdg-desktop-portal...";
|
QDBusMessage message = QDBusMessage::createMethodCall(
|
||||||
|
|
||||||
// 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
|
|
||||||
"org.freedesktop.portal.Desktop",
|
"org.freedesktop.portal.Desktop",
|
||||||
"/org/freedesktop/portal/desktop",
|
"/org/freedesktop/portal/desktop",
|
||||||
"org.freedesktop.portal.ScreenCast",
|
"org.freedesktop.portal.ScreenCast",
|
||||||
nullptr, // GCancellable
|
"SelectSources"
|
||||||
nullptr // GError
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (proxy == nullptr) {
|
QVariantMap options;
|
||||||
qCritical() << "Failed to connect to xdg-desktop-portal ScreenCast service. Make sure xdg-desktop-portal is running.";
|
options.insert("multiple", false); // Request single source
|
||||||
return -1;
|
message << "" << options; // parent_window (empty), options
|
||||||
}
|
|
||||||
|
|
||||||
// Call the ScreenCast.PickSource method
|
QDBusPendingCall pendingCall = sessionBus.asyncCall(message); // Use asyncCall
|
||||||
// Arguments: parent_window (empty string for no parent), options (dictionary)
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
|
||||||
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);
|
|
||||||
|
|
||||||
g_dbus_proxy_call(
|
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, handlePortalResponse);
|
||||||
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();
|
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user