feat: Implement startup retry mechanism for custom stations and equalizer persistence
- Added state management for startup retry and custom station handling in `EstadoRadio`. - Created tasks for implementing strict TDD with RED tests for HTTP failure retries and EQ persistence. - Developed verification report to ensure compliance with TDD practices. - Introduced fake services for testing, including `FakeServicioAudio`, `FakeServicioFavoritos`, and `FakeServicioRadio`. - Implemented widget tests for `PantallaInicio` and `PantallaFavoritos` to validate UI behavior with custom stations. - Enhanced `ServicioRadio` to support host rotation and retry logic for API calls. - Established a new configuration file to enforce project constraints and testing rules.
This commit is contained in:
@@ -2,15 +2,16 @@ import 'dart:developer' as developer;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
|
||||
/// Estado de reproducción expuesto al UI.
|
||||
enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Handler global — inicializado en main.dart con AudioService.init
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
PluriWaveAudioHandler? _handlerGlobal;
|
||||
|
||||
void registrarHandler(PluriWaveAudioHandler handler) {
|
||||
@@ -73,7 +74,7 @@ class ServicioAudio {
|
||||
bool get estaSonando => _handler.playbackState.value.playing;
|
||||
Future<void> dispose() async {}
|
||||
|
||||
// ── Ecualizador ──────────────────────────────────────────────────────────
|
||||
// ── Ecualizador ───────────────────────────────────────────────────────────
|
||||
AndroidEqualizer? get ecualizador => _handler.ecualizador;
|
||||
bool get ecualizadorDisponible => _handler.ecualizadorDisponible;
|
||||
PresetEcualizador get presetActual => _handler.presetActual;
|
||||
@@ -85,9 +86,9 @@ class ServicioAudio {
|
||||
_handler.setBanda(index, db);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// AudioHandler
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
final AndroidEqualizer _eq = AndroidEqualizer();
|
||||
|
||||
@@ -132,9 +133,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
playbackState.add(playbackState.value.copyWith(bufferedPosition: pos));
|
||||
});
|
||||
|
||||
// ── Escuchar errores de ExoPlayer ─────────────────────────────────────
|
||||
// Captura todos los PlaybackException: TYPE_SOURCE (HTTP cleartext,
|
||||
// certificado inválido, 404), TYPE_UNEXPECTED, timeout de conexión, etc.
|
||||
_player.playbackEventStream.listen(
|
||||
(_) {},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
@@ -143,8 +141,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
);
|
||||
}
|
||||
|
||||
/// Gestiona cualquier error de reproducción de ExoPlayer de forma
|
||||
/// controlada: emite estado de error al UI y resetea la reproducción.
|
||||
/// Gestiona cualquier error de reproducción de ExoPlayer.
|
||||
void _gestionarErrorReproduccion(Object error) {
|
||||
String mensaje;
|
||||
String codigoLog;
|
||||
@@ -160,17 +157,17 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
developer.log(
|
||||
'[PluriWave] Error reproducción: $codigoLog',
|
||||
name: 'ServicioAudio',
|
||||
level: 900, // warning
|
||||
level: 900,
|
||||
);
|
||||
|
||||
// Emitir estado de error al UI (incluye mensaje legible)
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState: AudioProcessingState.error,
|
||||
playing: false,
|
||||
errorMessage: mensaje,
|
||||
));
|
||||
emisoraActual = null;
|
||||
mediaItem.add(null);
|
||||
|
||||
// Resetear el player a estado idle limpio (sin lanzar otra excepción)
|
||||
_player.stop().catchError((_) {});
|
||||
}
|
||||
|
||||
@@ -178,7 +175,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
String _mensajeAmigable(PlayerException e) {
|
||||
final code = e.code;
|
||||
|
||||
// ERROR_CODE_IO_* — problemas de red/fuente
|
||||
if (code >= 2000 && code < 3000) {
|
||||
if (code == 2001) return 'Sin conexión a internet';
|
||||
if (code == 2002) return 'La URL de la radio no es válida';
|
||||
@@ -187,18 +183,14 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
return 'No se puede conectar a la radio';
|
||||
}
|
||||
|
||||
// ERROR_CODE_PARSING_* — formato de stream no soportado
|
||||
if (code >= 3000 && code < 4000) {
|
||||
return 'Formato de stream no compatible';
|
||||
}
|
||||
|
||||
// ERROR_CODE_DECODING_* — error de decodificación
|
||||
if (code >= 4000 && code < 5000) {
|
||||
return 'Error al decodificar el stream de audio';
|
||||
}
|
||||
|
||||
// TYPE_SOURCE — error en la fuente (HTTP cleartext, cert, etc.)
|
||||
// En just_audio suele mapearse como code=-1 o message con "Cleartext"
|
||||
final msg = e.message ?? '';
|
||||
if (msg.contains('Cleartext') || msg.contains('cleartext')) {
|
||||
return 'Esta radio usa HTTP sin cifrar (no permitido)';
|
||||
@@ -227,12 +219,9 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
await _player.stop();
|
||||
await _player.setUrl(mediaItem.id);
|
||||
await _player.play();
|
||||
// Habilitar ecualizador tras reproducir (necesita audio activo)
|
||||
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
|
||||
await _activarEcualizador();
|
||||
} on PlayerException catch (e) {
|
||||
// El error ya llega por playbackEventStream.onError, pero también
|
||||
// lo capturamos aquí para asegurarnos de emitir el estado de error
|
||||
// y propagarlo como excepción (para que EstadoRadio muestre el mensaje).
|
||||
_gestionarErrorReproduccion(e);
|
||||
throw Exception(_mensajeAmigable(e));
|
||||
} on Exception catch (e) {
|
||||
@@ -246,6 +235,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
playing: false,
|
||||
errorMessage: 'Error inesperado al reproducir',
|
||||
));
|
||||
emisoraActual = null;
|
||||
this.mediaItem.add(null);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -318,4 +309,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
await stop();
|
||||
await _player.dispose();
|
||||
}
|
||||
|
||||
Emisora _emisoraDesdeMediaItem(MediaItem mediaItem) {
|
||||
final uuid = mediaItem.extras?['uuid'] as String? ?? mediaItem.id;
|
||||
return Emisora(
|
||||
uuid: uuid,
|
||||
nombre: mediaItem.title,
|
||||
url: mediaItem.id,
|
||||
pais: (mediaItem.artist?.isNotEmpty ?? false) ? mediaItem.artist : null,
|
||||
favicon: mediaItem.artUri?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user