From dc9cbd6d84efb3934c406621925eb42b7bc360a2 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 8 Oct 2026 15:22:10 +0200 Subject: [PATCH] revert: drop the portal response timeout Hyprland's portal shows its picker during SelectSources, so a timeout before the picker step cut users off while choosing. A wedged portal backend looks the same; recording stays in Waiting for capture and the app must prompt. --- crates/clipbin-obs/src/ffi.rs | 18 --------- crates/clipbin-obs/src/recorder.rs | 62 ++---------------------------- docs/validation.md | 15 +++++--- 3 files changed, 12 insertions(+), 83 deletions(-) diff --git a/crates/clipbin-obs/src/ffi.rs b/crates/clipbin-obs/src/ffi.rs index f4eb268..8248763 100644 --- a/crates/clipbin-obs/src/ffi.rs +++ b/crates/clipbin-obs/src/ffi.rs @@ -176,14 +176,6 @@ fn pipewire_state(message: &str) -> Option { PipeWireState::Stopped }); } - // Requested from the portal, then the picker is shown (or a restore - // token is used): only the first step can stall without the user. - if message == "PipeWire initialized" { - return Some(PipeWireState::Requesting); - } - if message.starts_with("[pipewire] Asking for ") { - return Some(PipeWireState::Selecting); - } // The user denied or cancelled the portal request, or it failed. let refused = message.starts_with("[pipewire] Error ") || (message.starts_with("[pipewire] Failed to ") @@ -193,8 +185,6 @@ fn pipewire_state(message: &str) -> Option { fn load_pipewire_state() -> Option { match PIPEWIRE_STATE.load(Ordering::Relaxed) { - state if state == PipeWireState::Requesting as u8 => Some(PipeWireState::Requesting), - state if state == PipeWireState::Selecting as u8 => Some(PipeWireState::Selecting), state if state == PipeWireState::Streaming as u8 => Some(PipeWireState::Streaming), state if state == PipeWireState::Stopped as u8 => Some(PipeWireState::Stopped), state if state == PipeWireState::Refused as u8 => Some(PipeWireState::Refused), @@ -887,14 +877,6 @@ mod tests { state("[pipewire] Error selecting screencast source: Timeout"), Some(PipeWireState::Refused) ); - assert_eq!( - state("PipeWire initialized"), - Some(PipeWireState::Requesting) - ); - assert_eq!( - state("[pipewire] Asking for monitor and window"), - Some(PipeWireState::Selecting) - ); assert_eq!(state("[pipewire] Screencast session created"), None); // OBS cancelling its own pending request at teardown is not a refusal. assert_eq!(state("[portals] Request cancelled"), None); diff --git a/crates/clipbin-obs/src/recorder.rs b/crates/clipbin-obs/src/recorder.rs index 4c91862..1505f64 100644 --- a/crates/clipbin-obs/src/recorder.rs +++ b/crates/clipbin-obs/src/recorder.rs @@ -286,15 +286,11 @@ pub struct Health { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PipeWireState { - /// A screen-share session was requested from the portal. - Requesting = 1, - /// The portal is showing its picker or restoring an earlier selection. - Selecting = 2, - Streaming = 3, + Streaming = 1, /// The stream exists but is not delivering frames. - Stopped = 4, + Stopped = 2, /// The portal request was denied, cancelled by the user, or failed. - Refused = 5, + Refused = 3, } /// Derives the status shown to the user from health samples and output events. @@ -309,7 +305,6 @@ pub struct StatusTracker { stall_limit: Duration, last_written: u64, last_progress: Instant, - requesting_since: Option, } impl StatusTracker { @@ -317,9 +312,6 @@ impl StatusTracker { /// be finalised rather than failing mid-write. pub const DEFAULT_MIN_FREE_BYTES: u64 = 1 << 30; - /// How long the portal may take to reach its picker or restore a selection. - pub const PORTAL_RESPONSE_LIMIT: Duration = Duration::from_secs(10); - /// Matroska output can stay buffered until its segment closes, so footage /// may appear on disk only once per segment. A stall is declared after two /// segment lengths without growth. @@ -330,7 +322,6 @@ impl StatusTracker { stall_limit: (segment_length * 2 + Duration::from_secs(1)).max(Duration::from_secs(3)), last_written: 0, last_progress: now, - requesting_since: None, } } @@ -352,21 +343,6 @@ impl StatusTracker { self.last_written = health.written_bytes; self.last_progress = now; } - // Before the picker appears the portal needs no user input, so a - // request stuck there has stalled (seen with Hyprland's portal). - if health.pipewire == Some(PipeWireState::Requesting) { - let since = *self.requesting_since.get_or_insert(now); - if now.duration_since(since) >= Self::PORTAL_RESPONSE_LIMIT { - self.status = RecordingStatus::Failed { - reason: - "the screen-share portal did not respond; select a capture source again" - .into(), - }; - return &self.status; - } - } else { - self.requesting_since = None; - } if health.pipewire == Some(PipeWireState::Refused) { self.status = RecordingStatus::Failed { reason: "screen capture was not allowed: the source selection was denied, cancelled or failed" @@ -642,38 +618,6 @@ mod tests { )); } - #[test] - fn a_portal_that_never_reaches_its_picker_fails() { - let start = Instant::now(); - let mut tracker = StatusTracker::new(start, 0, SEGMENT); - let requesting = Health { - source_width: 0, - source_height: 0, - pipewire: Some(PipeWireState::Requesting), - ..healthy(5000) - }; - assert_eq!( - tracker.sample(&requesting, start), - &RecordingStatus::WaitingForCapture - ); - // The user may take as long as they like in the picker. - let selecting = Health { - pipewire: Some(PipeWireState::Selecting), - ..requesting.clone() - }; - let much_later = start + StatusTracker::PORTAL_RESPONSE_LIMIT * 3; - assert_eq!( - tracker.sample(&selecting, much_later), - &RecordingStatus::WaitingForCapture - ); - let mut tracker = StatusTracker::new(start, 0, SEGMENT); - tracker.sample(&requesting, start); - assert!(matches!( - tracker.sample(&requesting, start + StatusTracker::PORTAL_RESPONSE_LIMIT), - RecordingStatus::Failed { reason } if reason.contains("did not respond") - )); - } - #[test] fn output_errors_fail_and_clean_stops_go_idle() { let mut tracker = StatusTracker::new(Instant::now(), 0, SEGMENT); diff --git a/docs/validation.md b/docs/validation.md index 9a47008..d6c42ea 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -9,7 +9,7 @@ pass/fail results. | Where | Script | Status | | --- | --- | --- | | Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 9 scenarios pass on devbox (Intel UHD 770, VA-API) | -| Linux, real session (CachyOS, Hyprland, RX 9070 XT, 1440p60) | `scripts/validate/linux-hardware.sh` | 2026-10-08: H.264, HEVC and AV1 usable; restore token works; portal stall fix awaiting re-run | +| Linux, real session (CachyOS, Hyprland, RX 9070 XT, 1440p60) | `scripts/validate/linux-hardware.sh` | 2026-10-08: H.264, HEVC and AV1 usable; restore token works | | Windows (Game Capture, NVENC/AMF/QSV) | `scripts/validate/windows.ps1` | Not run yet; script untested | ## Probe commands @@ -81,11 +81,14 @@ CachyOS 7.2.9, Hyprland, RX 9070 XT, 1440p60, OBS 32.0.4: - The restore token works: the restored run started recording in 2 s without showing the picker. -- The portal stalled silently on one run again: it created the session but - never answered `SelectSources`, before any picker. Recording now fails if - the portal has not reached its picker 10 s after the request - (`StatusTracker::PORTAL_RESPONSE_LIMIT`); time spent in the picker itself - is not limited. +- Hyprland's portal backend (xdg-desktop-portal-hyprland) wedged in + `SelectSources` during this run and answered nothing afterwards, until + restarted; the earlier "no selection" runs were likely the same hang. + xdph shows its picker during `SelectSources`, so a hang there looks the same + as a user still choosing. A 10 s timeout tried in afc54b3 would cut users + off mid-pick and was removed. Recording stays honestly in `Waiting for + capture`; the app needs a prompt to choose a source (or restart the portal) + while it waits. - The motion check sampled a segment from before capture started (the user took 16 s in the picker); it now uses the segment with the most motion. - A portrait monitor (Hyprland transform 3) is captured as 1440x2560 and