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.
This commit is contained in:
@@ -176,14 +176,6 @@ fn pipewire_state(message: &str) -> Option<PipeWireState> {
|
|||||||
PipeWireState::Stopped
|
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.
|
// The user denied or cancelled the portal request, or it failed.
|
||||||
let refused = message.starts_with("[pipewire] Error ")
|
let refused = message.starts_with("[pipewire] Error ")
|
||||||
|| (message.starts_with("[pipewire] Failed to ")
|
|| (message.starts_with("[pipewire] Failed to ")
|
||||||
@@ -193,8 +185,6 @@ fn pipewire_state(message: &str) -> Option<PipeWireState> {
|
|||||||
|
|
||||||
fn load_pipewire_state() -> Option<PipeWireState> {
|
fn load_pipewire_state() -> Option<PipeWireState> {
|
||||||
match PIPEWIRE_STATE.load(Ordering::Relaxed) {
|
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::Streaming as u8 => Some(PipeWireState::Streaming),
|
||||||
state if state == PipeWireState::Stopped as u8 => Some(PipeWireState::Stopped),
|
state if state == PipeWireState::Stopped as u8 => Some(PipeWireState::Stopped),
|
||||||
state if state == PipeWireState::Refused as u8 => Some(PipeWireState::Refused),
|
state if state == PipeWireState::Refused as u8 => Some(PipeWireState::Refused),
|
||||||
@@ -887,14 +877,6 @@ mod tests {
|
|||||||
state("[pipewire] Error selecting screencast source: Timeout"),
|
state("[pipewire] Error selecting screencast source: Timeout"),
|
||||||
Some(PipeWireState::Refused)
|
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);
|
assert_eq!(state("[pipewire] Screencast session created"), None);
|
||||||
// OBS cancelling its own pending request at teardown is not a refusal.
|
// OBS cancelling its own pending request at teardown is not a refusal.
|
||||||
assert_eq!(state("[portals] Request cancelled"), None);
|
assert_eq!(state("[portals] Request cancelled"), None);
|
||||||
|
|||||||
@@ -286,15 +286,11 @@ pub struct Health {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PipeWireState {
|
pub enum PipeWireState {
|
||||||
/// A screen-share session was requested from the portal.
|
Streaming = 1,
|
||||||
Requesting = 1,
|
|
||||||
/// The portal is showing its picker or restoring an earlier selection.
|
|
||||||
Selecting = 2,
|
|
||||||
Streaming = 3,
|
|
||||||
/// The stream exists but is not delivering frames.
|
/// The stream exists but is not delivering frames.
|
||||||
Stopped = 4,
|
Stopped = 2,
|
||||||
/// The portal request was denied, cancelled by the user, or failed.
|
/// 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.
|
/// Derives the status shown to the user from health samples and output events.
|
||||||
@@ -309,7 +305,6 @@ pub struct StatusTracker {
|
|||||||
stall_limit: Duration,
|
stall_limit: Duration,
|
||||||
last_written: u64,
|
last_written: u64,
|
||||||
last_progress: Instant,
|
last_progress: Instant,
|
||||||
requesting_since: Option<Instant>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusTracker {
|
impl StatusTracker {
|
||||||
@@ -317,9 +312,6 @@ impl StatusTracker {
|
|||||||
/// be finalised rather than failing mid-write.
|
/// be finalised rather than failing mid-write.
|
||||||
pub const DEFAULT_MIN_FREE_BYTES: u64 = 1 << 30;
|
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
|
/// 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
|
/// may appear on disk only once per segment. A stall is declared after two
|
||||||
/// segment lengths without growth.
|
/// segment lengths without growth.
|
||||||
@@ -330,7 +322,6 @@ impl StatusTracker {
|
|||||||
stall_limit: (segment_length * 2 + Duration::from_secs(1)).max(Duration::from_secs(3)),
|
stall_limit: (segment_length * 2 + Duration::from_secs(1)).max(Duration::from_secs(3)),
|
||||||
last_written: 0,
|
last_written: 0,
|
||||||
last_progress: now,
|
last_progress: now,
|
||||||
requesting_since: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,21 +343,6 @@ impl StatusTracker {
|
|||||||
self.last_written = health.written_bytes;
|
self.last_written = health.written_bytes;
|
||||||
self.last_progress = now;
|
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) {
|
if health.pipewire == Some(PipeWireState::Refused) {
|
||||||
self.status = RecordingStatus::Failed {
|
self.status = RecordingStatus::Failed {
|
||||||
reason: "screen capture was not allowed: the source selection was denied, cancelled or 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]
|
#[test]
|
||||||
fn output_errors_fail_and_clean_stops_go_idle() {
|
fn output_errors_fail_and_clean_stops_go_idle() {
|
||||||
let mut tracker = StatusTracker::new(Instant::now(), 0, SEGMENT);
|
let mut tracker = StatusTracker::new(Instant::now(), 0, SEGMENT);
|
||||||
|
|||||||
+9
-6
@@ -9,7 +9,7 @@ pass/fail results.
|
|||||||
| Where | Script | Status |
|
| 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, 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 |
|
| Windows (Game Capture, NVENC/AMF/QSV) | `scripts/validate/windows.ps1` | Not run yet; script untested |
|
||||||
|
|
||||||
## Probe commands
|
## 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
|
- The restore token works: the restored run started recording in 2 s without
|
||||||
showing the picker.
|
showing the picker.
|
||||||
- The portal stalled silently on one run again: it created the session but
|
- Hyprland's portal backend (xdg-desktop-portal-hyprland) wedged in
|
||||||
never answered `SelectSources`, before any picker. Recording now fails if
|
`SelectSources` during this run and answered nothing afterwards, until
|
||||||
the portal has not reached its picker 10 s after the request
|
restarted; the earlier "no selection" runs were likely the same hang.
|
||||||
(`StatusTracker::PORTAL_RESPONSE_LIMIT`); time spent in the picker itself
|
xdph shows its picker during `SelectSources`, so a hang there looks the same
|
||||||
is not limited.
|
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
|
- 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.
|
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
|
- A portrait monitor (Hyprland transform 3) is captured as 1440x2560 and
|
||||||
|
|||||||
Reference in New Issue
Block a user