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:
@@ -253,32 +253,32 @@ Chain strategy: N/A (local apply)
|
||||
|
||||
### S7 pre-work: write failing tests
|
||||
|
||||
- [ ] **T-S7-01** [RED] Create `test/servicios/servicio_audio_reconnect_test.dart`:
|
||||
- Test A: backoff delay sequence for retries 1–5 is [1s, 2s, 4s, 8s, 16s] capped at maxDelay (S7-R7).
|
||||
- Test B: `_intencionReproducir=true` + stall → `reconectando` state emitted, reconnect scheduled (S7-R2-A, S7-R7).
|
||||
- Test C: `_intencionReproducir=false` + stall → NO reconnect (S7-R2-B, S7-R7).
|
||||
- Test D: after `maxRetries` exhausted → error state emitted (S7-R2-C, S7-R7).
|
||||
- Test E: successful reconnect resets retry counter (S7-R7).
|
||||
- Test F: user stop during stall cancels reconnect (S7-R6, S7-R7).
|
||||
**~70 lines.**
|
||||
- [ ] **T-S7-02** [RED] Add test in `test/servicios/servicio_audio_reconnect_test.dart`: buffer config (`AndroidLoadControl`) applied to player construction (S7-R1). **~15 lines.**
|
||||
- [ ] **T-S7-03** [RED] Add widget test `test/widgets/reconnect_ui_test.dart`: no `AlertDialog`/`SnackBar` shown while handler in `reconectando` state (S7-R3-A). **~20 lines.**
|
||||
- [x] **T-S7-01** [RED] Create `test/servicios/servicio_audio_reconnect_test.dart`:
|
||||
- Test A: backoff delay sequence for retries 1–5 is [1s, 2s, 4s, 8s, 16s] capped at maxDelay (S7-R7). **DONE (+ cap test with custom maxDelay).**
|
||||
- Test B: `_intencionReproducir=true` + stall → `reconectando` state emitted, reconnect scheduled (S7-R2-A, S7-R7). **DONE — decision/scheduling tested at `ControladorReconexion` level (extracted pure logic; the handler itself needs platform channels); state emission covered by the widget test.**
|
||||
- Test C: `_intencionReproducir=false` + stall → NO reconnect (S7-R2-B, S7-R7). **DONE.**
|
||||
- Test D: after `maxRetries` exhausted → error state emitted (S7-R2-C, S7-R7). **DONE (`DecisionReconexion.agotado` → handler falls through to existing terminal error path).**
|
||||
- Test E: successful reconnect resets retry counter (S7-R7). **DONE (`restablecer()` + backoff restarts at base).**
|
||||
- Test F: user stop during stall cancels reconnect (S7-R6, S7-R7). **DONE (`cancelar()` kills the pending timer; fired-after-cancel never retries).**
|
||||
**8 tests, RED captured first (load failure: controller file missing).**
|
||||
- [x] **T-S7-02** [RED] Add test in `test/servicios/servicio_audio_reconnect_test.dart`: buffer config (`AndroidLoadControl`) applied to player construction (S7-R1). **DONE — asserts `PluriWaveAudioHandler.configuracionCargaAndroid` values (15s/50s/2.5s/5s/prioritizeTime); construction wiring not unit-testable without platform channels, verified by code + on-device.**
|
||||
- [x] **T-S7-03** [RED] Add widget test `test/widgets/reconnect_ui_test.dart`: no `AlertDialog`/`SnackBar` shown while handler in `reconectando` state (S7-R3-A). **DONE — also asserts spinner + localized "Reconectando..." label and that the manual-retry button appears ONLY in error state.**
|
||||
|
||||
### S7 implementation
|
||||
|
||||
- [ ] **T-S7-04** [GREEN] Edit `lib/servicios/servicio_audio.dart` `_crearPlayer` (lines 159-163): pass `AudioLoadConfiguration(androidLoadControl: AndroidLoadControl(minBufferDuration: 15s, maxBufferDuration: 50s, bufferForPlaybackDuration: 2.5s, bufferForPlaybackAfterRebufferDuration: 5s, prioritizeTimeOverSizeThresholds: true))` at construction. Values extracted as named constants, NOT magic literals. **Reqs:** S7-R1. **~25 lines.**
|
||||
- [ ] **T-S7-05** [GREEN] Edit `lib/servicios/servicio_audio.dart`: add `EstadoReproduccion.reconectando` to the state enum (line 14). **Reqs:** S7-R2, S7-R3. **~3 lines.**
|
||||
- [ ] **T-S7-06** [GREEN] Edit `lib/servicios/servicio_audio.dart` `_gestionarErrorReproduccion` (lines 207-236) and `_eventosSub.onError` (lines 189-194): instead of transitioning immediately to terminal error when `_intencionReproducir == true` and error is network-class (2xxx range), enter the reconnect state machine — emit `reconectando`, schedule backoff retry using `_cambiarFuente` revision guard. Cancel/reset on user stop or source switch. After `maxRetries` exhaustion fall through to existing terminal error path. Configurable: `_maxRetries = 5`, `_baseDelay = 1s`, `_maxDelay = 30s`. **Reqs:** S7-R2. **~130 lines.**
|
||||
- [ ] **T-S7-07** [GREEN] Edit `lib/widgets/mini_reproductor.dart` and any player UI: map `EstadoReproduccion.reconectando` → buffering/loading indicator (NOT error dialog). **Reqs:** S7-R3. **~20 lines.**
|
||||
- [ ] **T-S7-08** [GREEN] Edit `lib/pantallas/pantalla_alarma_sonando.dart` (alarm pre-start / estadoStream listener): ensure `reconectando` is NOT treated as `reproduciendo`; the alarm's existing 12-second fallback timer remains authoritative. Add a comment documenting the boundary. **Reqs:** S7-R4. **~10 lines.**
|
||||
- [ ] **T-S7-09** [GREEN] Confirm `ServicioGrabacionRadio` error-handling code is NOT modified by S7 changes. Add inline comment referencing S7-R5 invariant. **Reqs:** S7-R5. **~3 lines (comment only).**
|
||||
- [x] **T-S7-04** [GREEN] `_crearPlayer` now passes `audioLoadConfiguration: configuracionCargaAndroid` (15s/50s/2.5s/5s, `prioritizeTimeOverSizeThresholds: true`) — values as named `static const` (`bufferMinimo`/`bufferMaximo`/`bufferParaIniciar`/`bufferTrasRebuffer`). API verified against installed just_audio 0.9.46 source: all params exist, no deviation. **Reqs:** S7-R1. **DONE.**
|
||||
- [x] **T-S7-05** [GREEN] `EstadoReproduccion.reconectando` added; `ServicioAudio.estadoStream` maps it from the handler's `reconectando` flag (after the terminal-error check, before cargando). **Reqs:** S7-R2, S7-R3. **DONE.**
|
||||
- [x] **T-S7-06** [GREEN] Reconnect state machine: pure backoff/decision logic extracted to NEW `lib/servicios/controlador_reconexion.dart` (`ControladorReconexion`, maxReintentos=5, base=1s, cap=30s, injectable timer factory); `_gestionarErrorReproduccion` enters it for network-class errors (`PlayerException` 2xxx OR `TimeoutException` from the 12s source guard) when intent=play; retries re-issue the source through the revision-guarded `_cambiarFuente` queue; success (`ready`+`playing`) resets; `pause`/`stop`/`playMediaItem` cancel/reset; exhaustion falls through to the single terminal error. `_cambiarFuente` completes normally when reconnect engaged so `EstadoRadio.reproducir` does NOT snackbar during retries (S7-R3). **Reqs:** S7-R2. **DONE.**
|
||||
- [x] **T-S7-07** [GREEN] `mini_reproductor.dart` (spinner for reconectando + `playbackStatusReconnecting` label in `_labelEstado`), `pantalla_reproductor.dart` (`_WaveHero` + `_Controles` treat reconectando as loading, not error), `visualizador_audio.dart` (reconectando keeps the visualizer active like cargando). NEW l10n key `playbackStatusReconnecting` in ALL 13 .arb locales + gen-l10n. **Reqs:** S7-R3. **DONE.**
|
||||
- [x] **T-S7-08** [GREEN] Boundary comment added at the `_estadoSub` listener in `pantalla_alarma_sonando.dart`: only `reproduciendo` cancels the 12s fallback timer; `reconectando` does NOT count as playing, WAV fallback stays authoritative. (Code already correct by construction — the listener checks `== reproduciendo`.) **Reqs:** S7-R4. **DONE.**
|
||||
- [x] **T-S7-09** [GREEN] `ServicioGrabacionRadio` error handling untouched; S7-R5 invariant comment added above `_fallar`. **Reqs:** S7-R5. **DONE.**
|
||||
|
||||
### S7 verification
|
||||
|
||||
- [ ] **T-S7-10** Run `flutter test test/servicios/servicio_audio_reconnect_test.dart test/widgets/reconnect_ui_test.dart`.
|
||||
- [ ] **T-S7-11** Run `flutter test` (full suite) — no regressions.
|
||||
- [ ] **T-S7-12** Run `flutter analyze` — zero errors.
|
||||
- [ ] **T-S7-13** Run `dart format lib/servicios/servicio_audio.dart lib/widgets/mini_reproductor.dart lib/pantallas/pantalla_alarma_sonando.dart`.
|
||||
- [x] **T-S7-10** Run `flutter test test/servicios/servicio_audio_reconnect_test.dart test/widgets/reconnect_ui_test.dart` — 10/10 green (RED captured first: `+0 -2` load failures).
|
||||
- [x] **T-S7-11** Run `flutter test` (full suite) — 99/99 passing (89 baseline + 10 new), no regressions.
|
||||
- [x] **T-S7-12** Run `flutter analyze` — `No issues found!`.
|
||||
- [x] **T-S7-13** Run `dart format` on all 9 touched Dart files (incl. new controller, pantalla_reproductor, visualizador, grabacion, both test files; 2 reflowed).
|
||||
|
||||
### S7 Definition of Done
|
||||
- `flutter test` green (all reconnect tests passing).
|
||||
|
||||
Reference in New Issue
Block a user