fix(reproduccion): robustez HTTP cleartext, errores ExoPlayer y certificados SSL
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:
ShanaiaBot
2026-04-04 20:43:56 +02:00
parent 5fd3d6deb9
commit 44849986d2
7 changed files with 220 additions and 33 deletions

View File

@@ -191,6 +191,7 @@ class _Artwork extends StatelessWidget {
builder: (context, snapshot) {
final reproduciendo = snapshot.data == EstadoReproduccion.reproduciendo;
final cargando = snapshot.data == EstadoReproduccion.cargando;
final hayError = snapshot.data == EstadoReproduccion.error;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
@@ -198,7 +199,15 @@ class _Artwork extends StatelessWidget {
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: reproduciendo
boxShadow: hayError
? [
BoxShadow(
color: theme.colorScheme.error.withValues(alpha: 0.25),
blurRadius: 12,
spreadRadius: 2,
),
]
: reproduciendo
? [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 0.4),
@@ -220,12 +229,15 @@ class _Artwork extends StatelessWidget {
fit: StackFit.expand,
children: [
// Logo / imagen
// errorWidget captura HandshakeException (cert autofirmado)
// y cualquier fallo de red en artwork. El error queda
// contenido aquí — no se propaga ni rompe el reproductor.
if (emisora.favicon != null && emisora.favicon!.isNotEmpty)
CachedNetworkImage(
imageUrl: emisora.favicon!,
fit: BoxFit.cover,
placeholder: (_, __) => _shimmer(theme),
errorWidget: (_, __, ___) => _iconoFallback(theme),
errorWidget: (_, url, error) => _iconoFallback(theme),
)
else
_iconoFallback(theme),
@@ -237,6 +249,18 @@ class _Artwork extends StatelessWidget {
child: CircularProgressIndicator(color: Colors.white),
),
),
// Overlay de error de reproducción
if (hayError)
Container(
color: Colors.black54,
child: Center(
child: Icon(
Icons.wifi_off_rounded,
size: 56,
color: Colors.white.withValues(alpha: 0.85),
),
),
),
],
),
),
@@ -310,6 +334,35 @@ class _Controles extends StatelessWidget {
final s = snapshot.data ?? EstadoReproduccion.detenido;
final reproduciendo = s == EstadoReproduccion.reproduciendo;
final cargando = s == EstadoReproduccion.cargando;
final hayError = s == EstadoReproduccion.error;
// En estado de error: mostrar mensaje y botón de reintento
if (hayError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline_rounded,
size: 40,
color: theme.colorScheme.error,
),
const SizedBox(height: 8),
Text(
'No se puede reproducir esta radio',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.error,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
FilledButton.tonalIcon(
icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text('Reintentar'),
onPressed: () => estado.reproducir(emisora),
),
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,