diff --git a/CMakeLists.txt b/CMakeLists.txt index cbef61b..bb8005a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,25 @@ find_package(Qt5Network REQUIRED) find_package(Qt5Widgets 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") 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") 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} +) diff --git a/wayland_poc.cpp b/wayland_poc.cpp new file mode 100644 index 0000000..7d763f6 --- /dev/null +++ b/wayland_poc.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include + +// PipeWire includes (will need to be installed on the system) +#include +#include +#include +#include +#include + +// 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"); + 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(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(); +}