feat(streaming): buffer resilience and automatic reconnection

- Construct the audio player with an enlarged live-stream buffer (15-50s forward cushion, 2.5s to start, 5s after rebuffer) so short network drops play through silently
- Add reconnect-on-stall state machine with bounded exponential backoff (1/2/4/8/16s, ~90s total window, 5 attempts) that re-prepares to the live edge; backoff/decision logic extracted to controlador_reconexion.dart as pure testable code
- Surface a new reconnecting playback state in the mini player and full player (localized in all 13 locales) instead of error dialogs during the retry window; a single friendly error appears only after exhaustion
- Guard interplay: user pause/stop cancels retries, audio interruptions cancel reconnect, alarm wake-up path keeps precedence, recording fails cleanly during drops
- Reset retry budget on station change; route stream timeouts through the network-error class
- 10 new tests (99 total green), flutter analyze clean
This commit is contained in:
2026-06-11 19:54:30 +02:00
parent 079e19f0ee
commit 0380bbb1e7
38 changed files with 743 additions and 38 deletions
+5 -1
View File
@@ -155,7 +155,10 @@ class _MiniReproductorState extends State<MiniReproductor> {
stream: estado.estadoStream,
builder: (context, snapshot) {
final s = snapshot.data ?? EstadoReproduccion.detenido;
if (s == EstadoReproduccion.cargando) {
// S7-R3: reconectando is a transient stall — render it like
// cargando (spinner), never as the error/retry affordance.
if (s == EstadoReproduccion.cargando ||
s == EstadoReproduccion.reconectando) {
return const SizedBox(
width: 48,
height: 48,
@@ -220,6 +223,7 @@ class _MiniReproductorState extends State<MiniReproductor> {
EstadoReproduccion.cargando => l10n.playbackStatusConnecting,
EstadoReproduccion.reproduciendo => l10n.playbackStatusLive,
EstadoReproduccion.pausado => l10n.playbackStatusPaused,
EstadoReproduccion.reconectando => l10n.playbackStatusReconnecting,
EstadoReproduccion.error => l10n.playbackStatusConnectionError,
EstadoReproduccion.detenido => l10n.playbackStatusStopped,
};
+2 -1
View File
@@ -68,7 +68,8 @@ class _VisualizadorAudioState extends State<VisualizadorAudio>
void _onEstado(EstadoReproduccion estado) {
final nuevoActivo =
estado == EstadoReproduccion.reproduciendo ||
estado == EstadoReproduccion.cargando;
estado == EstadoReproduccion.cargando ||
estado == EstadoReproduccion.reconectando;
if (!mounted) return;
if (nuevoActivo != _activo) {
setState(() => _activo = nuevoActivo);