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:
parent
902f88bb24
commit
cdc253bbc7
@ -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
253
wayland_poc.cpp
Normal 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();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user