UPnP: verify the Sonos queue landed, stop the phone double-downloading, and instrument the stutter #127

Merged
bvandeusen merged 4 commits from dev into main 2026-08-18 10:50:10 -04:00
Showing only changes of commit 0103953953 - Show all commits
@@ -33,20 +33,30 @@ class TransportFlapDetector(
*/
fun onChange(observation: TransportObservation): List<TransportObservation>? {
recent.addLast(observation)
while (recent.isNotEmpty() &&
observation.atElapsedMs - recent.first().atElapsedMs > windowMs
) {
recent.removeFirst()
}
if (recent.size < minChanges) return null
// One episode, one summary. A sustained fault would otherwise emit a
// summary per reading and bury the per-change events underneath them.
val since = lastSummaryAtMs
if (since != null && observation.atElapsedMs - since < summaryCooldownMs) return null
dropReadingsOlderThan(observation.atElapsedMs)
if (!isEpisode(observation.atElapsedMs)) return null
lastSummaryAtMs = observation.atElapsedMs
return recent.toList()
}
private fun dropReadingsOlderThan(nowMs: Long) {
while (recent.isNotEmpty() && nowMs - recent.first().atElapsedMs > windowMs) {
recent.removeFirst()
}
}
/**
* Enough changes packed together, and far enough from the last thing we
* wrote down. The cooldown is what keeps one episode to one summary: a
* sustained fault produces a change every poll, and a summary per reading
* would bury the per-change events underneath them.
*/
private fun isEpisode(nowMs: Long): Boolean {
val since = lastSummaryAtMs
val cooled = since == null || nowMs - since >= summaryCooldownMs
return recent.size >= minChanges && cooled
}
/** Forget everything — call when the route changes or casting ends. */
fun reset() {
recent.clear()