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:
@@ -64,6 +64,11 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
}
|
||||
_iniciarFadeIn();
|
||||
|
||||
// S7-R4 boundary: only `reproduciendo` cancels the fallback timer —
|
||||
// `reconectando`/`cargando` do NOT count as playing, so the 12-second
|
||||
// fallback below stays authoritative during the alarm ring. Waking the
|
||||
// user reliably beats reconnect persistence: if the radio is still
|
||||
// retrying when the timer fires, the bundled WAV takes over.
|
||||
_estadoSub = radio.estadoStream.listen((estado) {
|
||||
if (estado == EstadoReproduccion.reproduciendo && mounted) {
|
||||
_fallbackTimer?.cancel();
|
||||
|
||||
@@ -217,7 +217,10 @@ class _WaveHero extends StatelessWidget {
|
||||
stream: estadoStream,
|
||||
builder: (context, snapshot) {
|
||||
final reproduciendo = snapshot.data == EstadoReproduccion.reproduciendo;
|
||||
final cargando = snapshot.data == EstadoReproduccion.cargando;
|
||||
// S7-R3: reconectando renders as loading, never as error.
|
||||
final cargando =
|
||||
snapshot.data == EstadoReproduccion.cargando ||
|
||||
snapshot.data == EstadoReproduccion.reconectando;
|
||||
final hayError = snapshot.data == EstadoReproduccion.error;
|
||||
|
||||
return SizedBox(
|
||||
@@ -506,9 +509,9 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
? AppLocalizations.of(
|
||||
ctx,
|
||||
).durationMinutesOnly(opcion.duracion.inMinutes)
|
||||
: AppLocalizations.of(
|
||||
ctx,
|
||||
).durationSecondsOnly(opcion.duracion.inSeconds),
|
||||
: AppLocalizations.of(ctx).durationSecondsOnly(
|
||||
opcion.duracion.inSeconds,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarGrabacion(duracion: opcion.duracion);
|
||||
@@ -651,7 +654,10 @@ class _Controles extends StatelessWidget {
|
||||
builder: (context, snapshot) {
|
||||
final s = snapshot.data ?? EstadoReproduccion.detenido;
|
||||
final reproduciendo = s == EstadoReproduccion.reproduciendo;
|
||||
final cargando = s == EstadoReproduccion.cargando;
|
||||
// S7-R3: reconectando shows the loading spinner, not the error column.
|
||||
final cargando =
|
||||
s == EstadoReproduccion.cargando ||
|
||||
s == EstadoReproduccion.reconectando;
|
||||
final hayError = s == EstadoReproduccion.error;
|
||||
|
||||
if (hayError) {
|
||||
@@ -808,11 +814,9 @@ class _TimerWidget extends StatelessWidget {
|
||||
final s = t.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
final label =
|
||||
t.inHours > 0
|
||||
? AppLocalizations.of(context).durationHoursMinutesSeconds(
|
||||
t.inHours,
|
||||
m,
|
||||
s,
|
||||
)
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
).durationHoursMinutesSeconds(t.inHours, m, s)
|
||||
: AppLocalizations.of(context).durationMinutesSeconds(m, s);
|
||||
|
||||
return Row(
|
||||
|
||||
Reference in New Issue
Block a user