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_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}
|
||||
)
|
||||
|
||||
255
wayland_poc.cpp
255
wayland_poc.cpp
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user