feat: Add Wayland screen capture POC using PipeWire

This commit introduces a standalone proof-of-concept (POC) for screen
capture on Wayland using PipeWire and xdg-desktop-portal.

The POC demonstrates:
- Initializing PipeWire context and core.
- Requesting screen capture permission via xdg-desktop-portal, which
  triggers a user consent dialog.
- Receiving the PipeWire node ID for the selected screen/window.
- Connecting a PipeWire stream to the capture node.
- Processing a single video frame from the PipeWire stream.
- Converting the raw frame data into a QImage and saving it as a PNG file.

This POC serves as a foundational step towards adapting the Hyperion
grabber to work natively on Wayland, replacing the existing X11-specific
grabbing logic.

Further work will involve integrating this logic into the main grabber
application and handling continuous frame processing.
This commit is contained in:
Tobias J. Endres 2025-08-14 03:02:54 +02:00
parent 902f88bb24
commit cdc253bbc7
2 changed files with 271 additions and 0 deletions

View File

@ -11,7 +11,25 @@ find_package(Qt5Network REQUIRED)
find_package(Qt5Widgets REQUIRED) find_package(Qt5Widgets REQUIRED)
find_package(X11 REQUIRED) find_package(X11 REQUIRED)
# 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)
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})
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_executable(wayland_poc "wayland_poc.cpp")
target_link_libraries(wayland_poc
Qt5::Core
${PIPEWIRE_LIBRARIES}
${PIPEWIRE_SM_LIBRARIES}
${GLIB2_LIBRARIES}
)

253
wayland_poc.cpp Normal file
View File

@ -0,0 +1,253 @@
#include <QCoreApplication>
#include <QDebug>
#include <QImage>
#include <QBuffer>
#include <QFile>
// 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>
// 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");
return;
}
buf = b->buffer;
if (buf->datas[0].data == nullptr) {
pw_log_error("buffer has no data");
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);
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) {
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);
return;
}
guint32 node_id = g_variant_get_uint32(node_id_variant);
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.";
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
pw_init(nullptr, nullptr);
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.";
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
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.ScreenCast",
nullptr, // GCancellable
nullptr // GError
);
if (proxy == nullptr) {
qCritical() << "Failed to connect to xdg-desktop-portal ScreenCast service. Make sure xdg-desktop-portal is running.";
return -1;
}
// 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);
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();
return app.exec();
}