revert: drop the portal response timeout
Rust checks / windows-cross-check (push) Successful in 45s
Rust checks / linux (push) Failing after 5m42s

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.
This commit is contained in:
2026-10-08 15:22:10 +02:00
parent afc54b363e
commit dc9cbd6d84
3 changed files with 12 additions and 83 deletions
-18
View File
@@ -176,14 +176,6 @@ fn pipewire_state(message: &str) -> Option<PipeWireState> {
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<PipeWireState> {
fn load_pipewire_state() -> Option<PipeWireState> {
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);
+3 -59
View File
@@ -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<Instant>,
}
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);