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:
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/widgets/mini_reproductor.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
EstadoRadio _estadoRadio(FakeServicioAudio audio) {
|
||||
return EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// S7-R3-A: while the handler is reconnecting, the UI shows a loading
|
||||
/// indicator — never an error dialog or snackbar per retry attempt.
|
||||
void main() {
|
||||
testWidgets(
|
||||
'estado reconectando muestra indicador de carga, sin dialogo ni snackbar',
|
||||
(tester) async {
|
||||
final audio = FakeServicioAudio();
|
||||
final estado = _estadoRadio(audio);
|
||||
addTearDown(estado.dispose);
|
||||
await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno'));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(body: MiniReproductor()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
audio.emitirEstado(EstadoReproduccion.reconectando);
|
||||
// Two pumps: one delivers the stream event, one rebuilds the frame.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(find.byType(SnackBar), findsNothing);
|
||||
expect(
|
||||
find.byType(CircularProgressIndicator),
|
||||
findsOneWidget,
|
||||
reason: 'reconectando se presenta como carga, no como error',
|
||||
);
|
||||
expect(
|
||||
find.byIcon(Icons.refresh_rounded),
|
||||
findsNothing,
|
||||
reason: 'el boton de reintento manual es solo para el estado error',
|
||||
);
|
||||
expect(find.text('Reconectando...'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('el estado error si muestra el boton de reintento manual', (
|
||||
tester,
|
||||
) async {
|
||||
final audio = FakeServicioAudio();
|
||||
final estado = _estadoRadio(audio);
|
||||
addTearDown(estado.dispose);
|
||||
await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno'));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(body: MiniReproductor()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
audio.emitirEstado(EstadoReproduccion.error);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(find.byIcon(Icons.refresh_rounded), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user