class StallDetector { StallDetector({this.threshold = const Duration(seconds: 15)}) : assert(threshold > Duration.zero); final Duration threshold; Duration? _lastPos; Duration? _lastBuffer; int? _lastBytes; Duration _lastAdvanceAt = Duration.zero; bool _fired = false; bool update({ required Duration position, required Duration bufferedPosition, required int? bytesDownloaded, required bool isActivelyPlaying, required Duration now, }) { if (!isActivelyPlaying) { _lastPos = position; _lastBuffer = bufferedPosition; _lastBytes = bytesDownloaded; _lastAdvanceAt = now; return false; } if (_lastPos == null) { _lastPos = position; _lastBuffer = bufferedPosition; _lastBytes = bytesDownloaded; _lastAdvanceAt = now; return false; } final positionAdvanced = position != _lastPos; final bytesBecameAvailable = bytesDownloaded != null && _lastBytes == null; final hasComparableByteSamples = bytesDownloaded != null && _lastBytes != null; final bytesAdvanced = hasComparableByteSamples && bytesDownloaded != _lastBytes; final bufferAdvanced = bufferedPosition != _lastBuffer; final hasBufferedMedia = bufferedPosition - position > const Duration(seconds: 2); final transportAdvanced = bytesBecameAvailable || (hasComparableByteSamples ? bytesAdvanced : bufferAdvanced || hasBufferedMedia); final progressed = positionAdvanced || transportAdvanced; _lastPos = position; _lastBuffer = bufferedPosition; _lastBytes = bytesDownloaded; if (progressed) { _lastAdvanceAt = now; _fired = false; return false; } if (_fired) return false; if (now - _lastAdvanceAt >= threshold) { _fired = true; return true; } return false; } void reset() { _lastPos = null; _lastBuffer = null; _lastBytes = null; _lastAdvanceAt = Duration.zero; _fired = false; } }