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>? { fun onChange(observation: TransportObservation): List<TransportObservation>? {
recent.addLast(observation) recent.addLast(observation)
while (recent.isNotEmpty() && dropReadingsOlderThan(observation.atElapsedMs)
observation.atElapsedMs - recent.first().atElapsedMs > windowMs if (!isEpisode(observation.atElapsedMs)) return null
) {
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
lastSummaryAtMs = observation.atElapsedMs lastSummaryAtMs = observation.atElapsedMs
return recent.toList() 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. */ /** Forget everything — call when the route changes or casting ends. */
fun reset() { fun reset() {
recent.clear() recent.clear()