diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index bba9fbf3740..2d9eeb281f3 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -124,12 +124,19 @@ namespace portal { guint subscription_id; }; + struct shared_state_t { + std::atomic negotiated_width {0}; + std::atomic negotiated_height {0}; + std::atomic stream_dead {false}; + }; + struct stream_data_t { struct pw_stream *stream; struct spa_hook stream_listener; struct spa_video_info format; struct pw_buffer *current_buffer; uint64_t drm_format; + std::shared_ptr shared; }; struct dmabuf_format_info_t { @@ -683,9 +690,10 @@ namespace portal { pw_thread_loop_destroy(loop); } - void init(int stream_fd, int stream_node) { + void 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); @@ -700,6 +708,17 @@ namespace portal { pw_thread_loop_unlock(loop); } + void cleanup_stream() { + if (loop && stream_data.stream) { + pw_thread_loop_lock(loop); + pw_stream_disconnect(stream_data.stream); + 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) { pw_thread_loop_lock(loop); if (!stream_data.stream) { @@ -834,6 +853,31 @@ namespace portal { .error = on_core_error_cb, }; + static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) { + auto *d = static_cast(user_data); + + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + // If we hit an actual error or unconnected, it's always dead. + if (d->shared) { + d->shared->stream_dead.store(true, std::memory_order_relaxed); + } + break; + case PW_STREAM_STATE_PAUSED: + // Trigger a reinit to identify if changes occurred + if (d->shared && old == PW_STREAM_STATE_STREAMING) { + d->shared->stream_dead.store(true, std::memory_order_relaxed); + } + break; + case PW_STREAM_STATE_CONNECTING: + case PW_STREAM_STATE_STREAMING: + default: + break; + } + return; + } + static void on_process(void *user_data) { const auto d = static_cast(user_data); struct pw_buffer *b = nullptr; @@ -887,6 +931,19 @@ namespace portal { BOOST_LOG(info) << "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; + int physical_h = d->format.info.raw.size.height; + + if (d->shared) { + int old_w = d->shared->negotiated_width.load(std::memory_order_relaxed); + int old_h = d->shared->negotiated_height.load(std::memory_order_relaxed); + + if (physical_w != old_w || physical_h != old_h) { + d->shared->negotiated_width.store(physical_w, std::memory_order_relaxed); + d->shared->negotiated_height.store(physical_h, std::memory_order_relaxed); + } + } + uint64_t drm_format = 0; for (const auto &fmt : format_map) { if (fmt.fourcc == 0) { @@ -920,6 +977,7 @@ namespace portal { constexpr static const struct pw_stream_events stream_events = { .version = PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, .param_changed = on_param_changed, .process = on_process, }; @@ -945,7 +1003,46 @@ namespace portal { framerate = config.framerate; - pipewire.init(pipewire_fd, pipewire_node); + shared_state = std::make_shared(); + + pipewire.init(pipewire_fd, pipewire_node, shared_state); + + // 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); + + int timeout_ms = 1500; + int negotiated_w = 0; + int negotiated_h = 0; + + while (timeout_ms > 0) { + negotiated_w = shared_state->negotiated_width.load(); + negotiated_h = shared_state->negotiated_height.load(); + if (negotiated_w > 0 && negotiated_h > 0) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + timeout_ms -= 10; + } + + // Check previous logical dimensions + if (previous_width.load() == width && + previous_height.load() == height) { + if (capture_running.load()) { + stream_stopped.store(true); + } + } else { + previous_width.store(width); + previous_height.store(height); + } + + if (negotiated_w > 0 && negotiated_h > 0 && + (negotiated_w != width || negotiated_h != height)) { + BOOST_LOG(info) << "Using negotiated resolution "sv + << negotiated_w << "x" << negotiated_h; + + width = negotiated_w; + height = negotiated_h; + } return 0; } @@ -992,8 +1089,30 @@ namespace portal { pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia); 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)) { + pipewire.cleanup_stream(); + + // Add a small delay before reinit to let WirePlumber see state change + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 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); + return platf::capture_e::interrupted; + } else { + BOOST_LOG(warning) << "PipeWire stream disconnected. Forcing session reset."sv; + return platf::capture_e::reinit; + } + } + auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -1012,6 +1131,7 @@ namespace portal { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::interrupted: + capture_running.store(false); return status; case platf::capture_e::timeout: push_captured_image_cb(std::move(img_out), false); @@ -1163,6 +1283,11 @@ namespace portal { std::chrono::nanoseconds delay; 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