#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(); }