fix: time out a stalled portal and sample motion across segments
Hyprland's portal sometimes creates the session and never answers source selection, before any picker appears. Recording now fails if the portal has not reached its picker within 10 s; time in the picker is not limited. The motion check uses the segment with the most motion, since capture can start late.
This commit is contained in:
@@ -286,11 +286,15 @@ pub struct Health {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PipeWireState {
|
||||
Streaming = 1,
|
||||
/// 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,
|
||||
/// The stream exists but is not delivering frames.
|
||||
Stopped = 2,
|
||||
Stopped = 4,
|
||||
/// The portal request was denied, cancelled by the user, or failed.
|
||||
Refused = 3,
|
||||
Refused = 5,
|
||||
}
|
||||
|
||||
/// Derives the status shown to the user from health samples and output events.
|
||||
@@ -305,6 +309,7 @@ pub struct StatusTracker {
|
||||
stall_limit: Duration,
|
||||
last_written: u64,
|
||||
last_progress: Instant,
|
||||
requesting_since: Option<Instant>,
|
||||
}
|
||||
|
||||
impl StatusTracker {
|
||||
@@ -312,6 +317,9 @@ 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.
|
||||
@@ -322,6 +330,7 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +352,21 @@ 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"
|
||||
@@ -618,6 +642,38 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user