diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index badb4be2936..9bc5bfda1bf 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -93,7 +93,7 @@ namespace portal { if (file.is_open()) { std::getline(file, *token_); if (!token_->empty()) { - BOOST_LOG(info) << "Loaded portal restore token from disk"sv; + BOOST_LOG(info) << "[portalgrab] Loaded portal restore token from disk"sv; } } } @@ -105,9 +105,9 @@ namespace portal { std::ofstream file(get_file_path()); if (file.is_open()) { file << *token_; - BOOST_LOG(info) << "Saved portal restore token to disk"sv; + BOOST_LOG(info) << "[portalgrab] Saved portal restore token to disk"sv; } else { - BOOST_LOG(warning) << "Failed to save portal restore token"sv; + BOOST_LOG(warning) << "[portalgrab] Failed to save portal restore token"sv; } } @@ -172,6 +172,55 @@ namespace portal { int n_modifiers; }; + struct pipewire_streaminfo_t { + int pipewire_node; + int width; + int height; + int pos_x; + int pos_y; + std::string monitor_name; + + std::string to_display_name() { + if (monitor_name.length() > 0) { + return std::format("n{}", monitor_name); + } + return std::format("p{},{},{},{}", pos_x, pos_y, width, height); + } + + bool match_display_name(const std::string &display_name) const { + // Empty display_name never matches + if (display_name.empty()) { + return false; + } + // Method 1: Display_name starts with 'n' match by monitor_name starting from pos 1 + if (display_name[0] == 'n') { + return display_name.substr(1) == monitor_name; + } + // Method 2: Display_name starts with 'p' match by position+resolution starting from pos 1 + if (display_name[0] == 'p') { + auto stringstream = std::stringstream(display_name.substr(1)); + std::string stringvalue; + std::vector values; + constexpr char display_name_delimiter = ','; + while (std::getline(stringstream, stringvalue, display_name_delimiter)) { + if (std::ranges::all_of(stringvalue, ::isdigit)) { + values.emplace_back(std::stoi(stringvalue)); + } else { + BOOST_LOG(debug) << "[portalgrab] Failed to parse int value: '"sv << stringvalue << "'"; + } + } + // Check if the vector has 4 values (pos_x, pos_y, width, height) from display_names formatting + if (values.size() != 4) { + BOOST_LOG(debug) << "[portalgrab] Display name does not match expected format 'x,y,w,h': '"sv << display_name << "'"; + return false; + } + return pos_x == values[0] && pos_y == values[1] && width == values[2] && height == values[3]; + } + // All matching methods have failed. No match! + return false; + } + }; + class dbus_t { public: ~dbus_t() noexcept { @@ -194,17 +243,20 @@ namespace portal { ); if (err) { - BOOST_LOG(warning) << "Failed to explicitly close portal session: "sv << err->message; + BOOST_LOG(warning) << "[portalgrab] Failed to explicitly close portal session: "sv << err->message; } else { - BOOST_LOG(debug) << "Explicitly closed portal session: "sv << session_handle; + BOOST_LOG(debug) << "[portalgrab] Explicitly closed portal session: "sv << session_handle; } } } catch (const std::exception &e) { - BOOST_LOG(error) << "Standard exception caught in ~dbus_t: "sv << e.what(); + BOOST_LOG(error) << "[portalgrab] Standard exception caught in ~dbus_t: "sv << e.what(); } catch (...) { - BOOST_LOG(error) << "Unknown exception caught in ~dbus_t"sv; + BOOST_LOG(error) << "[portalgrab] Unknown exception caught in ~dbus_t"sv; } + if (pipewire_fd >= 0) { + close(pipewire_fd); + } if (screencast_proxy) { g_clear_object(&screencast_proxy); } @@ -237,11 +289,11 @@ namespace portal { void finalize_portal_security() { #if !defined(__FreeBSD__) - BOOST_LOG(debug) << "Finalizing Portal security: dropping capabilities and resetting dumpable"sv; + BOOST_LOG(debug) << "[portalgrab] Finalizing Portal security: dropping capabilities and resetting dumpable"sv; cap_t caps = cap_get_proc(); if (!caps) { - BOOST_LOG(error) << "Failed to get process capabilities"sv; + BOOST_LOG(error) << "[portalgrab] Failed to get process capabilities"sv; return; } @@ -252,14 +304,14 @@ namespace portal { cap_set_flag(caps, CAP_PERMITTED, permitted_list.size(), permitted_list.data(), CAP_CLEAR); if (cap_set_proc(caps) != 0) { - BOOST_LOG(error) << "Failed to prune capabilities: "sv << std::strerror(errno); + BOOST_LOG(error) << "[portalgrab] Failed to prune capabilities: "sv << std::strerror(errno); } cap_free(caps); // Reset dumpable AFTER the caps have been pruned to ensure the Portal can // access /proc/pid/root. if (prctl(PR_SET_DUMPABLE, 1) != 0) { - BOOST_LOG(error) << "Failed to set PR_SET_DUMPABLE: "sv << std::strerror(errno); + BOOST_LOG(error) << "[portalgrab] Failed to set PR_SET_DUMPABLE: "sv << std::strerror(errno); } #endif } @@ -281,7 +333,7 @@ namespace portal { return -1; } - if (start_portal_session(loop, session_path, pipewire_node, width, height, use_screencast_only) < 0) { + if (start_portal_session(loop, session_path, pipewire_streams, use_screencast_only) < 0) { return -1; } @@ -300,14 +352,14 @@ namespace portal { } if (select_remote_desktop_devices(loop, *session_path) < 0) { - BOOST_LOG(warning) << "RemoteDesktop.SelectDevices failed, falling back to ScreenCast-only mode"sv; + BOOST_LOG(warning) << "[portalgrab] RemoteDesktop.SelectDevices failed, falling back to ScreenCast-only mode"sv; g_free(*session_path); *session_path = nullptr; return false; } if (select_screencast_sources(loop, *session_path, false) < 0) { - BOOST_LOG(warning) << "ScreenCast.SelectSources failed with RemoteDesktop session, trying ScreenCast-only mode"sv; + BOOST_LOG(warning) << "[portalgrab] ScreenCast.SelectSources failed with RemoteDesktop session, trying ScreenCast-only mode"sv; g_free(*session_path); *session_path = nullptr; return false; @@ -331,10 +383,35 @@ namespace portal { return 0; } + bool is_session_closed() const { + if (conn && !session_handle.empty()) { + // Try to retrieve property org.freedesktop.portal.Session::version + g_autoptr(GError) err = nullptr; + g_dbus_connection_call_sync( + conn, + "org.freedesktop.portal.Desktop", + session_handle.c_str(), + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new("(ss)", "org.freedesktop.portal.Session", "version"), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + &err + ); + // If we cannot get the property then the session portal was closed. + if (err) { + BOOST_LOG(debug) << "[portalgrab] Session closed as check failed: "sv << err->message; + return true; + } + } + // The session is not closed (or might not have been opened yet). + return false; + } + + std::vector pipewire_streams; int pipewire_fd; - int pipewire_node; - int width; - int height; private: GDBusConnection *conn; @@ -363,7 +440,7 @@ namespace portal { g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "CreateSession", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { - BOOST_LOG(error) << "Could not create "sv << session_type << " session: "sv << err->message; + BOOST_LOG(error) << "[portalgrab] Could not create "sv << session_type << " session: "sv << err->message; return -1; } @@ -374,7 +451,7 @@ namespace portal { g_autoptr(GVariant) create_response = dbus_response_wait(&response); if (!create_response) { - BOOST_LOG(error) << session_type << " CreateSession: no response received"sv; + BOOST_LOG(error) << "[portalgrab] " << session_type << " CreateSession: no response received"sv; return -1; } @@ -382,16 +459,16 @@ namespace portal { g_autoptr(GVariant) results = nullptr; g_variant_get(create_response, "(u@a{sv})", &response_code, &results); - BOOST_LOG(debug) << session_type << " CreateSession response_code: "sv << response_code; + BOOST_LOG(debug) << "[portalgrab] " << session_type << " CreateSession response_code: "sv << response_code; if (response_code != 0) { - BOOST_LOG(error) << session_type << " CreateSession failed with response code: "sv << response_code; + BOOST_LOG(error) << "[portalgrab] " << session_type << " CreateSession failed with response code: "sv << response_code; return -1; } g_autoptr(GVariant) session_handle_v = g_variant_lookup_value(results, "session_handle", nullptr); if (!session_handle_v) { - BOOST_LOG(error) << session_type << " CreateSession: session_handle not found in response"sv; + BOOST_LOG(error) << "[portalgrab] " << session_type << " CreateSession: session_handle not found in response"sv; return -1; } @@ -402,7 +479,7 @@ namespace portal { *session_path_out = g_strdup(g_variant_get_string(session_handle_v, nullptr)); } - BOOST_LOG(debug) << session_type << " CreateSession: got session handle: "sv << *session_path_out; + BOOST_LOG(debug) << "[portalgrab] " << session_type << " CreateSession: got session handle: "sv << *session_path_out; // Save it for the destructor to use during cleanup this->session_handle = *session_path_out; return 0; @@ -431,7 +508,7 @@ namespace portal { g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(remote_desktop_proxy, "SelectDevices", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { - BOOST_LOG(error) << "Could not select devices: "sv << err->message; + BOOST_LOG(error) << "[portalgrab] Could not select devices: "sv << err->message; return -1; } @@ -442,16 +519,16 @@ namespace portal { g_autoptr(GVariant) devices_response = dbus_response_wait(&response); if (!devices_response) { - BOOST_LOG(error) << "SelectDevices: no response received"sv; + BOOST_LOG(error) << "[portalgrab] SelectDevices: no response received"sv; return -1; } guint32 response_code; g_variant_get(devices_response, "(u@a{sv})", &response_code, nullptr); - BOOST_LOG(debug) << "SelectDevices response_code: "sv << response_code; + BOOST_LOG(debug) << "[portalgrab] SelectDevices response_code: "sv << response_code; if (response_code != 0) { - BOOST_LOG(error) << "SelectDevices failed with response code: "sv << response_code; + BOOST_LOG(error) << "[portalgrab] SelectDevices failed with response code: "sv << response_code; return -1; } @@ -472,6 +549,7 @@ namespace portal { g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token)); g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(SOURCE_TYPE_MONITOR)); g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(CURSOR_MODE_EMBEDDED)); + g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(TRUE)); if (persist) { g_variant_builder_add(&builder, "{sv}", "persist_mode", g_variant_new_uint32(PERSIST_UNTIL_REVOKED)); if (!restore_token_t::empty()) { @@ -483,7 +561,7 @@ namespace portal { g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(screencast_proxy, "SelectSources", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { - BOOST_LOG(error) << "Could not select sources: "sv << err->message; + BOOST_LOG(error) << "[portalgrab] Could not select sources: "sv << err->message; return -1; } @@ -494,23 +572,23 @@ namespace portal { g_autoptr(GVariant) sources_response = dbus_response_wait(&response); if (!sources_response) { - BOOST_LOG(error) << "SelectSources: no response received"sv; + BOOST_LOG(error) << "[portalgrab] SelectSources: no response received"sv; return -1; } guint32 response_code; g_variant_get(sources_response, "(u@a{sv})", &response_code, nullptr); - BOOST_LOG(debug) << "SelectSources response_code: "sv << response_code; + BOOST_LOG(debug) << "[portalgrab] SelectSources response_code: "sv << response_code; if (response_code != 0) { - BOOST_LOG(error) << "SelectSources failed with response code: "sv << response_code; + BOOST_LOG(error) << "[portalgrab] SelectSources failed with response code: "sv << response_code; return -1; } return 0; } - int start_portal_session(GMainLoop *loop, const gchar *session_path, int &out_pipewire_node, int &out_width, int &out_height, bool use_screencast) { + int start_portal_session(GMainLoop *loop, const gchar *session_path, std::vector &out_pipewire_streams, bool use_screencast) { GDBusProxy *proxy = use_screencast ? screencast_proxy : remote_desktop_proxy; const char *session_type = use_screencast ? "ScreenCast" : "RemoteDesktop"; @@ -531,7 +609,7 @@ namespace portal { g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_sync(proxy, "Start", g_variant_builder_end(&builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); if (err) { - BOOST_LOG(error) << "Could not start "sv << session_type << " session: "sv << err->message; + BOOST_LOG(error) << "[portalgrab] Could not start "sv << session_type << " session: "sv << err->message; return -1; } @@ -542,7 +620,7 @@ namespace portal { g_autoptr(GVariant) start_response = dbus_response_wait(&response); if (!start_response) { - BOOST_LOG(error) << session_type << " Start: no response received"sv; + BOOST_LOG(error) << "[portalgrab] " << session_type << " Start: no response received"sv; return -1; } @@ -551,16 +629,16 @@ namespace portal { g_autoptr(GVariant) streams = nullptr; g_variant_get(start_response, "(u@a{sv})", &response_code, &dict); - BOOST_LOG(debug) << session_type << " Start response_code: "sv << response_code; + BOOST_LOG(debug) << "[portalgrab] " << session_type << " Start response_code: "sv << response_code; if (response_code != 0) { - BOOST_LOG(error) << session_type << " Start failed with response code: "sv << response_code; + BOOST_LOG(error) << "[portalgrab] " << session_type << " Start failed with response code: "sv << response_code; return -1; } streams = g_variant_lookup_value(dict, "streams", G_VARIANT_TYPE("a(ua{sv})")); if (!streams) { - BOOST_LOG(error) << session_type << " Start: no streams in response"sv; + BOOST_LOG(error) << "[portalgrab] " << session_type << " Start: no streams in response"sv; return -1; } @@ -570,10 +648,30 @@ namespace portal { } GVariantIter iter; + const auto wl_monitors = wl::monitors(); + int out_pipewire_node; g_autoptr(GVariant) value = nullptr; g_variant_iter_init(&iter, streams); while (g_variant_iter_next(&iter, "(u@a{sv})", &out_pipewire_node, &value)) { + int out_width; + int out_height; g_variant_lookup(value, "size", "(ii)", &out_width, &out_height, nullptr); + + int out_pos_x; + int out_pos_y; + g_variant_lookup(value, "position", "(ii)", &out_pos_x, &out_pos_y, nullptr); + + auto stream = pipewire_streaminfo_t {out_pipewire_node, out_width, out_height, out_pos_x, out_pos_y}; + + // Try to match the stream to a monitor_name by position/resolution and update stream info + for (const auto &monitor : wl_monitors) { + if (monitor->viewport.offset_x == out_pos_x && monitor->viewport.offset_y == out_pos_y && monitor->viewport.logical_width == out_width && monitor->viewport.logical_height == out_height) { + stream.monitor_name = monitor->name; + break; + } + } + + out_pipewire_streams.emplace_back(stream); } return 0; @@ -586,7 +684,7 @@ namespace portal { g_autoptr(GError) err = nullptr; g_autoptr(GVariant) reply = g_dbus_proxy_call_with_unix_fd_list_sync(screencast_proxy, "OpenPipeWireRemote", msg, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &fd_list, nullptr, &err); if (err) { - BOOST_LOG(error) << "Could not open pipewire remote: "sv << err->message; + BOOST_LOG(error) << "[portalgrab] Could not open pipewire remote: "sv << err->message; return -1; } @@ -652,89 +750,13 @@ namespace portal { }; /** - * @brief Singleton cache for portal session data. + * @brief Singleton cache for persistent portalgrab session data. * - * This prevents creating multiple portal sessions during encoder probing, - * which would show multiple screen recording indicators in the system tray. */ class session_cache_t { public: static session_cache_t &instance(); - /** - * @brief Get or create a portal session. - * - * If a cached session exists and is valid, returns the cached data. - * Otherwise, creates a new session and caches it. - * - * @return 0 on success, -1 on failure - */ - int get_or_create_session(int &pipewire_fd, int &pipewire_node, int &width, int &height) { - std::scoped_lock lock(mutex_); - - if (valid_) { - // Return cached session data - pipewire_fd = dup(pipewire_fd_); // Duplicate FD for each caller - pipewire_node = pipewire_node_; - width = width_; - height = height_; - BOOST_LOG(debug) << "Reusing cached portal session"sv; - return 0; - } - - // Create new session - dbus_ = std::make_unique(); - if (dbus_->init() < 0) { - return -1; - } - if (dbus_->connect_to_portal() < 0) { - dbus_.reset(); - return -1; - } - - // Cache the session data - pipewire_fd_ = dbus_->pipewire_fd; - pipewire_node_ = dbus_->pipewire_node; - width_ = dbus_->width; - height_ = dbus_->height; - valid_ = true; - - // Return to caller (duplicate FD so each caller has their own) - pipewire_fd = dup(pipewire_fd_); - pipewire_node = pipewire_node_; - width = width_; - height = height_; - - BOOST_LOG(debug) << "Created new portal session (cached)"sv; - return 0; - } - - /** - * @brief Invalidate the cached session. - * - * Call this when the session becomes invalid (e.g., on error). - */ - void invalidate() noexcept { - try { - std::scoped_lock lock(mutex_); - if (valid_) { - BOOST_LOG(debug) << "Invalidating cached portal session"sv; - if (pipewire_fd_ >= 0) { - close(pipewire_fd_); - pipewire_fd_ = -1; - } - - dbus_.reset(); - - valid_ = false; - } - } catch (const std::exception &e) { - BOOST_LOG(error) << "Exception during session invalidation: "sv << e.what(); - } catch (...) { - BOOST_LOG(error) << "Unknown error during session invalidation"sv; - } - } - bool is_maxframerate_failed() const { return maxframerate_failed_; } @@ -746,23 +768,10 @@ namespace portal { private: session_cache_t() = default; - ~session_cache_t() { - if (pipewire_fd_ >= 0) { - close(pipewire_fd_); - } - } - // Prevent copying session_cache_t(const session_cache_t &) = delete; session_cache_t &operator=(const session_cache_t &) = delete; - std::mutex mutex_; - std::unique_ptr dbus_; - int pipewire_fd_ = -1; - int pipewire_node_ = 0; - int width_ = 0; - int height_ = 0; - bool valid_ = false; bool maxframerate_failed_ = false; }; @@ -776,22 +785,33 @@ namespace portal { public: pipewire_t(): loop(pw_thread_loop_new("Pipewire thread", nullptr)) { + BOOST_LOG(debug) << "[portalgrab] Start PW thread loop"sv; pw_thread_loop_start(loop); } ~pipewire_t() { + BOOST_LOG(debug) << "[portalgrab] Destroying pipewire_t"sv; if (loop) { + BOOST_LOG(debug) << "[portalgrab] Stop PW thread loop"sv; pw_thread_loop_stop(loop); } - cleanup_stream(); + try { + cleanup_stream(); + } catch (const std::exception &e) { + BOOST_LOG(error) << "[portalgrab] Standard exception caught in ~pipewire_t: "sv << e.what(); + } catch (...) { + BOOST_LOG(error) << "[portalgrab] Unknown exception caught in ~pipewire_t"sv; + } pw_thread_loop_lock(loop); if (core) { + BOOST_LOG(debug) << "[portalgrab] Disconnect PW core"sv; pw_core_disconnect(core); core = nullptr; } if (context) { + BOOST_LOG(debug) << "[portalgrab] Destroy PW context"sv; pw_context_destroy(context); context = nullptr; } @@ -799,8 +819,12 @@ namespace portal { pw_thread_loop_unlock(loop); if (fd >= 0) { + BOOST_LOG(debug) << "[portalgrab] Close pipewire_fd"sv; close(fd); } + BOOST_LOG(debug) << "[portalgrab] Stop PW thread loop"sv; + pw_thread_loop_stop(loop); + BOOST_LOG(debug) << "[portalgrab] Destroy PW thread loop"sv; pw_thread_loop_destroy(loop); } @@ -820,29 +844,39 @@ namespace portal { stream_data.frame_ready = ready; } - void init(int stream_fd, int stream_node, std::shared_ptr shared_state) { + int init(int stream_fd, int stream_node, std::shared_ptr shared_state) { fd = stream_fd; node = stream_node; stream_data.shared = std::move(shared_state); pw_thread_loop_lock(loop); - + BOOST_LOG(debug) << "[portalgrab] Setup PW context"sv; context = pw_context_new(pw_thread_loop_get_loop(loop), nullptr, 0); if (context) { - core = pw_context_connect_fd(context, dup(fd), nullptr, 0); + BOOST_LOG(debug) << "[portalgrab] Connect PW context to fd"sv; + core = pw_context_connect_fd(context, fd, nullptr, 0); if (core) { pw_core_add_listener(core, &core_listener, &core_events, nullptr); + } else { + BOOST_LOG(debug) << "[portalgrab] Failed to connect to PW core. Error: "sv << errno << "(" << strerror(errno) << ")"sv; + return -1; } + } else { + BOOST_LOG(debug) << "[portalgrab] Failed to setup PW context. Error: "sv << errno << "(" << strerror(errno) << ")"sv; + return -1; } pw_thread_loop_unlock(loop); + return 0; } void cleanup_stream() { + BOOST_LOG(debug) << "[portalgrab] Cleaning up stream"sv; if (loop && stream_data.stream) { pw_thread_loop_lock(loop); // 1. Lock the frame mutex to stop fill_img + BOOST_LOG(debug) << "[portalgrab] Stop fill_img"sv; { std::scoped_lock lock(stream_data.frame_mutex); stream_data.frame_ready = false; @@ -850,20 +884,29 @@ namespace portal { } if (stream_data.stream) { + BOOST_LOG(debug) << "[portalgrab] Disconnect stream"sv; + pw_stream_disconnect(stream_data.stream); + BOOST_LOG(debug) << "[portalgrab] Destroy stream"sv; pw_stream_destroy(stream_data.stream); stream_data.stream = nullptr; } pw_thread_loop_unlock(loop); } - session_cache_t::instance().invalidate(); } - void ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) { + int ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) { pw_thread_loop_lock(loop); if (!stream_data.stream) { + if (!core) { + BOOST_LOG(debug) << "[portalgrab] PW core not available. Cannot ensure stream."sv; + pw_thread_loop_unlock(loop); + return -1; + } + struct pw_properties *props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr); + BOOST_LOG(debug) << "[portalgrab] Create PW stream"sv; stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props); pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data); @@ -897,10 +940,11 @@ namespace portal { params[n_params] = format_param; n_params++; } - + BOOST_LOG(debug) << "[portalgrab] Connect PW stream - fd "sv << fd << " node "sv << node; pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params); } pw_thread_loop_unlock(loop); + return 0; } static void close_img_fds(egl::img_descriptor_t *img_descriptor) { @@ -1027,11 +1071,11 @@ namespace portal { } static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) { - BOOST_LOG(info) << "Connected to pipewire version "sv << pw_info->version; + BOOST_LOG(info) << "[portalgrab] Connected to pipewire version "sv << pw_info->version; } static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) { - BOOST_LOG(info) << "Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message; + BOOST_LOG(info) << "[portalgrab] Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message; } constexpr static const struct pw_core_events core_events = { @@ -1041,7 +1085,7 @@ namespace portal { }; static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) { - BOOST_LOG(debug) << "PipeWire stream state: " << pw_stream_state_as_string(old) + BOOST_LOG(debug) << "[portalgrab] PipeWire stream state: " << pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state); auto *d = static_cast(user_data); @@ -1060,7 +1104,7 @@ namespace portal { break; case PW_STREAM_STATE_ERROR: if (old != PW_STREAM_STATE_STREAMING && !session_cache_t::instance().is_maxframerate_failed()) { - BOOST_LOG(warning) << "Negotiation failed, will retry without maxFramerate"sv; + BOOST_LOG(warning) << "[portalgrab] Negotiation failed, will retry without maxFramerate"sv; session_cache_t::instance().set_maxframerate_failed(); } [[fallthrough]]; @@ -1145,13 +1189,13 @@ namespace portal { return; } - BOOST_LOG(info) << "Video format: "sv << d->format.info.raw.format; - BOOST_LOG(info) << "Size: "sv << d->format.info.raw.size.width << "x"sv << d->format.info.raw.size.height; + BOOST_LOG(info) << "[portalgrab] Video format: "sv << d->format.info.raw.format; + BOOST_LOG(info) << "[portalgrab] Size: "sv << d->format.info.raw.size.width << "x"sv << d->format.info.raw.size.height; if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) { - BOOST_LOG(info) << "Framerate (from compositor): 0/1 (variable rate capture)"; + BOOST_LOG(info) << "[portalgrab] Framerate (from compositor): 0/1 (variable rate capture)"; } else { - BOOST_LOG(info) << "Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom; - BOOST_LOG(info) << "Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom; + BOOST_LOG(info) << "[portalgrab] Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom; + BOOST_LOG(info) << "[portalgrab] Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom; } int physical_w = d->format.info.raw.size.width; @@ -1180,10 +1224,10 @@ namespace portal { uint32_t buffer_types = 0; if (spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier) != nullptr && d->drm_format) { - BOOST_LOG(info) << "using DMA-BUF buffers"sv; + BOOST_LOG(info) << "[portalgrab] using DMA-BUF buffers"sv; buffer_types |= 1 << SPA_DATA_DmaBuf; } else { - BOOST_LOG(info) << "using memory buffers"sv; + BOOST_LOG(info) << "[portalgrab] using memory buffers"sv; buffer_types |= 1 << SPA_DATA_MemPtr; } @@ -1224,10 +1268,10 @@ namespace portal { delay = std::chrono::nanoseconds( (static_cast(fps_strict.den) * 1'000'000'000LL) / fps_strict.num ); - BOOST_LOG(info) << "Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]"; + BOOST_LOG(info) << "[portalgrab] Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]"; } else { delay = std::chrono::nanoseconds {1s} / framerate; - BOOST_LOG(info) << "Requested frame rate [" << framerate << "fps]"; + BOOST_LOG(info) << "[portalgrab] Requested frame rate [" << framerate << "fps]"; } mem_type = hwdevice_type; @@ -1235,13 +1279,36 @@ namespace portal { return -1; } - // Use cached portal session to avoid creating multiple screen recordings - int pipewire_fd = -1; - int pipewire_node = 0; - if (session_cache_t::instance().get_or_create_session(pipewire_fd, pipewire_node, width, height) < 0) { + // Connect DBus portal session + if (dbus.init() < 0) { + BOOST_LOG(error) << "[portalgrab] Failed to connect to dbus. portal_t::init() failed."; + return -1; + } + if (dbus.connect_to_portal() < 0) { + BOOST_LOG(error) << "[portalgrab] Failed to connect to portal. portal_t::init() failed."; return -1; } + // Match display_name to a stream from the pipewire_streams vector + bool use_fallback = true; + pipewire_streaminfo_t stream; + for (const auto &stream_ : dbus.pipewire_streams) { + if (stream_.match_display_name(display_name)) { + stream = stream_; + use_fallback = false; + break; + } + } + // Fall back to first stream if we cannot match the given display_name to a stream in currently available streams. + if (use_fallback) { + BOOST_LOG(info) << "[portalgrab] Using first available stream as no matching stream was found for: '"sv << display_name << "'"; + stream = dbus.pipewire_streams[0]; + } + // Set values inherited from display_t + width = stream.width; + height = stream.height; + BOOST_LOG(info) << "[portalgrab] Streaming display '"sv << stream.monitor_name << "' from position: "sv << stream.pos_x << "x"sv << stream.pos_y << " resolution: "sv << width << "x"sv << height; + framerate = config.framerate; if (!shared_state) { @@ -1251,10 +1318,16 @@ namespace portal { shared_state->negotiated_width.store(0); shared_state->negotiated_height.store(0); } - pipewire.init(pipewire_fd, pipewire_node, shared_state); + if (pipewire.init(dbus.pipewire_fd, stream.pipewire_node, shared_state) < 0) { + BOOST_LOG(error) << "[portalgrab] Failed to init pipewire. portal_t::init() failed."; + return -1; + } // Start PipeWire now so format negotiation can proceed before capture start - pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia); + if (pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia) < 0) { + BOOST_LOG(error) << "[portalgrab] Failed to ensure pipewire stream. portal_t::init() failed."; + return -1; + } int timeout_ms = 1500; int negotiated_w = 0; @@ -1270,22 +1343,9 @@ namespace portal { timeout_ms -= 10; } - // Check previous logical dimensions - if (previous_width.load() == width && previous_height.load() == height) { - if (capture_running.load()) { - { - std::scoped_lock lock(pipewire.frame_mutex()); - stream_stopped.store(true); - } - pipewire.frame_cv().notify_all(); - } - } else { - previous_width.store(width); - previous_height.store(height); - } - + // Set width and height to the values negotiated by pipewire if (negotiated_w > 0 && negotiated_h > 0 && (negotiated_w != width || negotiated_h != height)) { - BOOST_LOG(info) << "Using negotiated resolution "sv + BOOST_LOG(info) << "[portalgrab] Using negotiated resolution "sv << negotiated_w << "x" << negotiated_h; width = negotiated_w; @@ -1309,7 +1369,7 @@ namespace portal { while (std::chrono::steady_clock::now() < deadline) { if (!wait_for_frame(deadline)) { - return stream_stopped.load() ? platf::capture_e::interrupted : platf::capture_e::timeout; + return platf::capture_e::timeout; } if (!pull_free_image_cb(img_out)) { @@ -1352,26 +1412,24 @@ namespace portal { platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia); + if (pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia) < 0) { + BOOST_LOG(error) << "[portalgrab] Failed to ensure pipewire stream. capture() failed with error."; + return platf::capture_e::error; + } sleep_overshoot_logger.reset(); - capture_running.store(true); while (true) { - // Check if PipeWire signaled a state change or error - if (stream_stopped.load() || shared_state->stream_dead.exchange(false)) { - // If stream is marked as stopped, clear state and send interrupted status - if (stream_stopped.load()) { - BOOST_LOG(warning) << "PipeWire stream stopped by user."sv; - capture_running.store(false); - stream_stopped.store(false); - previous_height.store(0); - previous_width.store(0); + // Check if PipeWire signaled a dead stream + if (shared_state->stream_dead.exchange(false)) { + // If the pipewire stream stopped due to closed portal session stop the capture with an error + if (dbus.is_session_closed()) { + BOOST_LOG(warning) << "[portalgrab] PipeWire stream stopped by closed portal session."sv; pipewire.frame_cv().notify_all(); return platf::capture_e::error; - } else { - BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv; - return platf::capture_e::reinit; } + // Re-init the capture if the stream is dead for any other reason + BOOST_LOG(warning) << "[portalgrab] PipeWire stream disconnected. Forcing session reset."sv; + return platf::capture_e::reinit; } // Advance to (or catch up with) next delay interval @@ -1391,30 +1449,28 @@ namespace portal { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: - capture_running.store(false); - stream_stopped.store(false); - previous_height.store(0); - previous_width.store(0); pipewire.frame_cv().notify_all(); return status; case platf::capture_e::timeout: if (!pull_free_image_cb(img_out)) { // Detect if shutdown is pending - BOOST_LOG(debug) << "PipeWire: timeout -> interrupt nudge"; - capture_running.store(false); - stream_stopped.store(false); - previous_height.store(0); - previous_width.store(0); + BOOST_LOG(debug) << "[portalgrab] PipeWire: timeout -> interrupt nudge"; pipewire.frame_cv().notify_all(); return platf::capture_e::interrupted; } - push_captured_image_cb(std::move(img_out), false); + if (!push_captured_image_cb(std::move(img_out), false)) { + BOOST_LOG(debug) << "[portalgrab] PipeWire: !push_captured_image_cb -> ok"; + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - push_captured_image_cb(std::move(img_out), true); + if (!push_captured_image_cb(std::move(img_out), true)) { + BOOST_LOG(debug) << "[portalgrab] PipeWire: !push_captured_image_cb -> ok"; + return platf::capture_e::ok; + } break; default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << std::to_underlying(status) << ']'; + BOOST_LOG(error) << "[portalgrab] Unrecognized capture status ["sv << std::to_underlying(status) << ']'; return status; } } @@ -1487,7 +1543,7 @@ namespace portal { img->sequence = ++sequence; if (retries > 0) { - BOOST_LOG(debug) << "Processed frame after " << retries << " redundant events."sv; + BOOST_LOG(debug) << "[portalgrab] Processed frame after " << retries << " redundant events."sv; } } @@ -1495,10 +1551,10 @@ namespace portal { std::unique_lock lock(pipewire.frame_mutex()); bool success = pipewire.frame_cv().wait_until(lock, deadline, [&] { - return pipewire.is_frame_ready() || stream_stopped.load() || shared_state->stream_dead.load(); + return pipewire.is_frame_ready() || shared_state->stream_dead.load(); }); - if (success && !stream_stopped.load()) { + if (success) { pipewire.set_frame_ready(false); return true; } @@ -1523,7 +1579,7 @@ namespace portal { eglQueryDmaBufFormatsEXT(egl_display, MAX_DMABUF_FORMATS, dmabuf_formats.data(), &num_dmabuf_formats); if (num_dmabuf_formats > MAX_DMABUF_FORMATS) { - BOOST_LOG(warning) << "Some DMA-BUF formats are being ignored"sv; + BOOST_LOG(warning) << "[portalgrab] Some DMA-BUF formats are being ignored"sv; } for (EGLint i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) { @@ -1537,7 +1593,7 @@ namespace portal { eglQueryDmaBufModifiersEXT(egl_display, dmabuf_formats[i], MAX_DMABUF_MODIFIERS, mods.data(), nullptr, &num_modifiers); if (num_modifiers > MAX_DMABUF_MODIFIERS) { - BOOST_LOG(warning) << "Some DMA-BUF modifiers are being ignored"sv; + BOOST_LOG(warning) << "[portalgrab] Some DMA-BUF modifiers are being ignored"sv; } dmabuf_infos[n_dmabuf_infos].format = pw_format; @@ -1577,13 +1633,13 @@ namespace portal { bool intel_present = check_intel("/sys/class/drm/card0/device/vendor") || check_intel("/sys/class/drm/card1/device/vendor"); if (intel_present) { - BOOST_LOG(info) << "Hybrid GPU system detected (Intel + discrete) - CUDA will use memory buffers"sv; + BOOST_LOG(info) << "[portalgrab] Hybrid GPU system detected (Intel + discrete) - CUDA will use memory buffers"sv; display_is_nvidia = false; } else { // No Intel GPU found, check if NVIDIA is present const char *vendor = eglQueryString(egl_display.get(), EGL_VENDOR); if (vendor && std::string_view(vendor).contains("NVIDIA")) { - BOOST_LOG(info) << "Pure NVIDIA system - DMA-BUF will be enabled for CUDA"sv; + BOOST_LOG(info) << "[portalgrab] Pure NVIDIA system - DMA-BUF will be enabled for CUDA"sv; display_is_nvidia = true; } } @@ -1598,6 +1654,7 @@ namespace portal { platf::mem_type_e mem_type; wl::display_t wl_display; + dbus_t dbus; pipewire_t pipewire; std::array dmabuf_infos; int n_dmabuf_infos; @@ -1607,10 +1664,6 @@ namespace portal { std::optional last_seq {}; std::uint64_t sequence {}; uint32_t framerate; - static inline std::atomic previous_height {0}; - static inline std::atomic previous_width {0}; - static inline std::atomic stream_stopped {false}; - static inline std::atomic capture_running {false}; std::shared_ptr shared_state; }; } // namespace portal @@ -1619,7 +1672,7 @@ namespace platf { std::shared_ptr portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { using enum platf::mem_type_e; if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda && hwdevice_type != vulkan) { - BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; + BOOST_LOG(error) << "[portalgrab] Could not initialize display with the given hw device type."sv; return nullptr; } @@ -1636,12 +1689,25 @@ namespace platf { auto dbus = std::make_shared(); if (dbus->init() < 0) { + BOOST_LOG(warning) << "[portalgrab] Failed to connect to dbus. Cannot enumerate displays, returning empty list."; return {}; } pw_init(nullptr, nullptr); - display_names.emplace_back("org.freedesktop.portal.Desktop"); + if (dbus->connect_to_portal() < 0) { + BOOST_LOG(warning) << "[portalgrab] Failed to connect to portal. Cannot enumerate displays, returning empty list."; + return {}; + } + + for (auto stream_ : dbus->pipewire_streams) { + BOOST_LOG(info) << "[portalgrab] Found stream for display: '"sv << stream_.monitor_name << "' position: "sv << stream_.pos_x << "x"sv << stream_.pos_y << " resolution: "sv << stream_.width << "x"sv << stream_.height; + display_names.emplace_back(stream_.to_display_name()); + } + // Release the portal session as soon as possible to properly release related resources early. + dbus.reset(); + + // Return currently active display names return display_names; } } // namespace platf