fix(offline): hysteresis + cheap probe + streaming liveness override (#467)

Long sessions died when a single transient blip flipped the app to offline and
the queue then skipped every cached-but-not-downloaded track. Harden the
detector:

- Consecutive-failure hysteresis: declare offline only after 2 failed polls,
  not a single blip.
- Replace the HEAD https://1.1.1.1 probe with a concurrent race of cheap
  generate_204 endpoints, so a saturated link (long Hi-Res tracks) no longer
  times the probe out.
- Positive liveness override: if a segment downloaded within the last 45s we
  are online by definition, regardless of the probe.

cc
This commit is contained in:
vicrodh committed 2026-05-22 21:46:33 -06:00
1 parent e4fa90c1b2
commit 133e90f752
3 files changed
+130 -67

No files matched your search

+27 -7
View File
@@ -73,6 +73,10 @@ struct ThrottleInner {
/// Timestamp of the most recent audio underrun. `None` means we have
/// not observed one this session.
last_underrun: Option<Instant>,
/// Timestamp of the most recent successful segment download. `None`
/// until the first segment lands. Used as a positive liveness signal
/// for the offline detector (issue #467).
last_successful_download: Option<Instant>,
}
pub struct ThrottleState {
@@ -92,14 +96,18 @@ impl ThrottleState {
/// Feed a fresh per-segment bandwidth measurement (MB/s). Called from
/// the CMAF streaming loop every few segments.
pub fn record_segment_bandwidth(&self, mbps: f64) {
if !mbps.is_finite() || mbps <= 0.0 {
return;
}
if let Ok(mut inner) = self.inner.write() {
inner.bandwidth_ema_mbps = Some(match inner.bandwidth_ema_mbps {
Some(prev) => prev * (1.0 - BANDWIDTH_EMA_ALPHA) + mbps * BANDWIDTH_EMA_ALPHA,
None => mbps,
});
// Liveness signal (issue #467): reaching this path means a segment
// batch just downloaded — bytes are flowing, so we are online,
// independent of whether the throughput sample is usable for the
// EMA. Recorded before the validity guard on purpose.
inner.last_successful_download = Some(Instant::now());
if mbps.is_finite() && mbps > 0.0 {
inner.bandwidth_ema_mbps = Some(match inner.bandwidth_ema_mbps {
Some(prev) => prev * (1.0 - BANDWIDTH_EMA_ALPHA) + mbps * BANDWIDTH_EMA_ALPHA,
None => mbps,
});
}
}
}
@@ -116,6 +124,18 @@ impl ThrottleState {
self.inner.read().ok().and_then(|i| i.bandwidth_ema_mbps)
}
/// Seconds since the last successful segment download, or `None` if no
/// bytes have been pulled this session. Positive liveness signal for the
/// offline detector (issue #467): if segments are flowing we are online
/// by definition, regardless of what a connectivity probe reports.
pub fn seconds_since_download(&self) -> Option<u64> {
self.inner
.read()
.ok()
.and_then(|i| i.last_successful_download)
.map(|t| t.elapsed().as_secs())
}
/// True when an underrun was recorded within `PANIC_WINDOW_SECS`.
pub fn in_panic_mode(&self) -> bool {
let inner = match self.inner.read() {