fix(reproduccion): robustez HTTP cleartext, errores ExoPlayer y certificados SSL
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
**Fix 1 — HTTP cleartext (streams sin HTTPS):** - Añadir android/app/src/main/res/xml/network_security_config.xml con cleartextTrafficPermitted=true para permitir streams de radio HTTP - Referenciar en AndroidManifest.xml con android:networkSecurityConfig - Resuelve: 'Cleartext HTTP traffic to [host] not permitted' en ExoPlayer - Radio Paradise (Dance Wave, HTTP) y otras radios HTTP funcionan ahora **Fix 2 — Gestión de error TYPE_SOURCE y todos los PlaybackException:** - Añadir listener en playbackEventStream.onError en PluriWaveAudioHandler - _gestionarErrorReproduccion() emite AudioProcessingState.error al UI, loggea el error y resetea el player a estado idle limpio - _mensajeAmigable() traduce códigos ERROR_CODE_IO_*, ERROR_CODE_PARSING_*, ERROR_CODE_DECODING_* y mensajes de Cleartext/HandshakeException a texto legible - EstadoRadio.reproducir() captura la excepción y cancela el timer si estaba activo - EstadoRadio escucha el estadoStream y cancela timer ante cualquier error **Fix 3 — Artwork con certificado autofirmado:** - errorWidget en CachedNetworkImage captura HandshakeException silenciosamente - Muestra _iconoFallback (icono de radio) en lugar de imagen rota - El error de artwork no se propaga ni interrumpe la reproducción **Fix 4 — UI consistente en estado de error:** - PantallaReproductor._Controles muestra mensaje + botón Reintentar en error - PantallaReproductor._Artwork muestra overlay wifi_off en estado de error - MiniReproductor muestra botón refresh (reintentar) en estado de error - EstadoReproduccion.error ya estaba definido; ahora el estadoStream lo emite - Timer cancelado automáticamente cuando la reproducción falla - Test de smoke corregido (boilerplate MyApp → placeholder válido) Fixes: cleartext HTTP, cert autofirmado, ExoPlayer TYPE_SOURCE, UI inconsistente
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
@@ -26,6 +28,9 @@ class ServicioAudio {
|
||||
|
||||
Stream<EstadoReproduccion> get estadoStream =>
|
||||
_handler.playbackState.map((s) {
|
||||
if (s.processingState == AudioProcessingState.error) {
|
||||
return EstadoReproduccion.error;
|
||||
}
|
||||
if (s.processingState == AudioProcessingState.loading ||
|
||||
s.processingState == AudioProcessingState.buffering) {
|
||||
return EstadoReproduccion.cargando;
|
||||
@@ -126,6 +131,83 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
_player.bufferedPositionStream.listen((pos) {
|
||||
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) {
|
||||
_gestionarErrorReproduccion(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Gestiona cualquier error de reproducción de ExoPlayer de forma
|
||||
/// controlada: emite estado de error al UI y resetea la reproducción.
|
||||
void _gestionarErrorReproduccion(Object error) {
|
||||
String mensaje;
|
||||
String codigoLog;
|
||||
|
||||
if (error is PlayerException) {
|
||||
codigoLog = 'PlayerException(code=${error.code}): ${error.message}';
|
||||
mensaje = _mensajeAmigable(error);
|
||||
} else {
|
||||
codigoLog = 'Error desconocido: $error';
|
||||
mensaje = 'Error de reproducción';
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'[PluriWave] Error reproducción: $codigoLog',
|
||||
name: 'ServicioAudio',
|
||||
level: 900, // warning
|
||||
);
|
||||
|
||||
// Emitir estado de error al UI (incluye mensaje legible)
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState: AudioProcessingState.error,
|
||||
playing: false,
|
||||
errorMessage: mensaje,
|
||||
));
|
||||
|
||||
// Resetear el player a estado idle limpio (sin lanzar otra excepción)
|
||||
_player.stop().catchError((_) {});
|
||||
}
|
||||
|
||||
/// Traduce códigos de error de ExoPlayer a mensajes para el usuario.
|
||||
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';
|
||||
if (code == 2003) return 'La radio no está disponible (error 404)';
|
||||
if (code == 2004) return 'Tiempo de espera agotado al conectar';
|
||||
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)';
|
||||
}
|
||||
if (msg.contains('CERTIFICATE') || msg.contains('HandshakeException')) {
|
||||
return 'Certificado SSL inválido en la radio';
|
||||
}
|
||||
|
||||
return 'No se puede reproducir esta radio';
|
||||
}
|
||||
|
||||
AudioProcessingState _mapProcState(ProcessingState state) {
|
||||
@@ -139,19 +221,30 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playMediaItem(MediaItem item) async {
|
||||
mediaItem.add(item);
|
||||
Future<void> playMediaItem(MediaItem mediaItem) async {
|
||||
this.mediaItem.add(mediaItem);
|
||||
try {
|
||||
await _player.stop();
|
||||
await _player.setUrl(item.id);
|
||||
await _player.setUrl(mediaItem.id);
|
||||
await _player.play();
|
||||
// Habilitar ecualizador tras reproducir (necesita audio activo)
|
||||
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) {
|
||||
developer.log(
|
||||
'[PluriWave] Error inesperado en playMediaItem: $e',
|
||||
name: 'ServicioAudio',
|
||||
level: 900,
|
||||
);
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState: AudioProcessingState.error,
|
||||
errorMessage: e.message ?? 'Error de reproducción',
|
||||
errorCode: e.code,
|
||||
playing: false,
|
||||
errorMessage: 'Error inesperado al reproducir',
|
||||
));
|
||||
rethrow;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user