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:
3 files changed
+130
-67
No files matched your search
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user