From 0e18c822928166cdf371767abb4a15d765c0fcab Mon Sep 17 00:00:00 2001 From: freetlab Date: Thu, 21 May 2026 20:52:28 +0200 Subject: [PATCH] fix(player): recreate audio player on station switch --- docs/audio-switching-notes.md | 6 +- lib/servicios/servicio_audio.dart | 120 ++++++++++++++++++++++++------ 2 files changed, 101 insertions(+), 25 deletions(-) diff --git a/docs/audio-switching-notes.md b/docs/audio-switching-notes.md index 5aa4049..3238e9d 100644 --- a/docs/audio-switching-notes.md +++ b/docs/audio-switching-notes.md @@ -7,19 +7,21 @@ Referencia interna para futuras correcciones del reproductor de PluriWave. Este - `AudioPlayer.play()` completa cuando la reproducción termina, se pausa o se detiene. En radio en vivo no representa simplemente “ya empezó a sonar”. - Las versiones antiguas de PluriWave que sí cambiaban de emisora usaban el flujo simple de `audio_service` + `just_audio`: `stop() -> setUrl() -> play()` dentro de `playMediaItem`. - La regresión apareció cuando mezclamos dos responsabilidades: usar `handler.emisoraActual` como estado técnico de audio y también como estado visual inmediato para mostrar el mini reproductor. -- En la versión vieja, el audio podía cambiar correctamente aunque `emisoraActual` no se usara como “selección visual inmediata”. Para arreglar el primer render del mini reproductor sin romper audio, conviene separar ambos conceptos. +- El logcat de 2026-05-21 mostró la media session de PluriWave atascada en `CONNECTING` sin `PlayerException`, con metadata de la emisora anterior (`Track FM`). Eso apunta a un player/ExoPlayer reutilizado que queda colgado entre `stop/setUrl/play`, no a un error HTTP visible. ## Decisión aplicada en PluriWave -- Restaurar el flujo histórico del handler: `stop -> setUrl -> play`. - Mantener la selección visual inmediata en `EstadoRadio` mediante una emisora seleccionada propia, separada del `emisoraActual` interno del handler. - No usar `setAudioSource(..., preload: false)` como reemplazo de `setUrl(...)`: en esta app rompió incluso la primera conexión. +- No esperar `play()` como operación de finalización para radio en vivo. +- Al cambiar emisora, recrear el `AudioPlayer`/ExoPlayer para matar completamente la reproducción anterior antes de `setUrl(...)`. - Proteger `EstadoRadio.reproducir` con revisión para que una operación vieja no aplique presets/clicks encima de una nueva. ## Intentos descartados - `setAudioSource(..., preload: false)`: teóricamente razonable, pero en PluriWave rompió la primera conexión. - Hacer que el handler publique `emisoraActual` antes de que el flujo histórico de audio avance: arregla el mini reproductor, pero cambia la semántica que tenían las versiones viejas. +- Reutilizar siempre el mismo `AudioPlayer` con `stop()`: logcat mostró estado `CONNECTING` persistente sin excepción al cambiar/reintentar. ## Fuentes consultadas diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart index 633801e..90f4b05 100644 --- a/lib/servicios/servicio_audio.dart +++ b/lib/servicios/servicio_audio.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:audio_service/audio_service.dart'; @@ -90,11 +91,16 @@ class ServicioAudio { // AudioHandler // ───────────────────────────────────────────────────────────────────────────── class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { - final AndroidEqualizer _eq = AndroidEqualizer(); + static const _timeoutCambioFuente = Duration(seconds: 12); + static const _timeoutCierrePlayer = Duration(seconds: 3); - late final AudioPlayer _player = AudioPlayer( - audioPipeline: AudioPipeline(androidAudioEffects: [_eq]), - ); + AndroidEqualizer _eq = AndroidEqualizer(); + late AudioPlayer _player = _crearPlayer(); + StreamSubscription? _estadoPlayerSub; + StreamSubscription? _bufferedSub; + StreamSubscription? _eventosSub; + Future _colaCambioFuente = Future.value(); + int _revisionFuente = 0; Emisora? emisoraActual; double _volumen = 1.0; @@ -108,11 +114,17 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { PresetEcualizador get presetActual => _presetActual; PluriWaveAudioHandler() { - _setupStreams(); + _conectarStreamsPlayer(); } - void _setupStreams() { - _player.playerStateStream.listen((state) { + AudioPlayer _crearPlayer() { + return AudioPlayer( + audioPipeline: AudioPipeline(androidAudioEffects: [_eq]), + ); + } + + void _conectarStreamsPlayer() { + _estadoPlayerSub = _player.playerStateStream.listen((state) { final playing = state.playing; final proc = state.processingState; playbackState.add(playbackState.value.copyWith( @@ -129,11 +141,11 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { )); }); - _player.bufferedPositionStream.listen((pos) { + _bufferedSub = _player.bufferedPositionStream.listen((pos) { playbackState.add(playbackState.value.copyWith(bufferedPosition: pos)); }); - _player.playbackEventStream.listen( + _eventosSub = _player.playbackEventStream.listen( (_) {}, onError: (Object error, StackTrace stackTrace) { _gestionarErrorReproduccion(error); @@ -214,33 +226,91 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future playMediaItem(MediaItem mediaItem) async { + final revision = ++_revisionFuente; + _colaCambioFuente = _colaCambioFuente + .catchError((_) {}) + .then((_) => _cambiarFuente(mediaItem, revision)); + return _colaCambioFuente; + } + + Future _cambiarFuente(MediaItem mediaItem, int revision) async { this.mediaItem.add(mediaItem); + emisoraActual = _emisoraDesdeMediaItem(mediaItem); + playbackState.add(playbackState.value.copyWith( + processingState: AudioProcessingState.loading, + playing: false, + errorMessage: null, + )); try { - await _player.stop(); - await _player.setUrl(mediaItem.id); - await _player.play(); - emisoraActual = _emisoraDesdeMediaItem(mediaItem); - await _activarEcualizador(); + await _recrearPlayer(); + if (revision != _revisionFuente) return; + + await _player.setUrl(mediaItem.id).timeout(_timeoutCambioFuente); + if (revision != _revisionFuente) return; + + _iniciarPlaySinBloquear(mediaItem, revision); + unawaited(_activarEcualizador()); } on PlayerException catch (e) { - _gestionarErrorReproduccion(e); + if (revision == _revisionFuente) { + _gestionarErrorReproduccion(e); + } throw Exception(_mensajeAmigable(e)); - } on Exception catch (e) { + } on Exception catch (e, stackTrace) { developer.log( '[PluriWave] Error inesperado en playMediaItem: $e', name: 'ServicioAudio', level: 900, + stackTrace: stackTrace, ); - playbackState.add(playbackState.value.copyWith( - processingState: AudioProcessingState.error, - playing: false, - errorMessage: 'Error inesperado al reproducir', - )); - emisoraActual = null; - this.mediaItem.add(null); + if (revision == _revisionFuente) { + playbackState.add(playbackState.value.copyWith( + processingState: AudioProcessingState.error, + playing: false, + errorMessage: 'Error inesperado al reproducir', + )); + emisoraActual = null; + this.mediaItem.add(null); + } rethrow; } } + Future _recrearPlayer() async { + await _estadoPlayerSub?.cancel(); + await _bufferedSub?.cancel(); + await _eventosSub?.cancel(); + + final anterior = _player; + try { + await anterior.stop().timeout(_timeoutCierrePlayer); + } catch (_) {} + try { + await anterior.dispose().timeout(_timeoutCierrePlayer); + } catch (_) {} + + _eq = AndroidEqualizer(); + _eqDisponible = false; + _player = _crearPlayer(); + await _player.setVolume(_volumen); + _conectarStreamsPlayer(); + } + + void _iniciarPlaySinBloquear(MediaItem mediaItem, int revision) { + unawaited( + _player.play().catchError((Object error, StackTrace stackTrace) { + developer.log( + '[PluriWave] Error al iniciar ${mediaItem.title}: $error', + name: 'ServicioAudio', + level: 900, + stackTrace: stackTrace, + ); + if (revision == _revisionFuente) { + _gestionarErrorReproduccion(error); + } + }), + ); + } + Future _activarEcualizador() async { try { final params = await _eq.parameters; @@ -295,6 +365,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future stop() async { + _revisionFuente++; await _player.stop(); emisoraActual = null; mediaItem.add(null); @@ -307,6 +378,9 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future onTaskRemoved() async { await stop(); + await _estadoPlayerSub?.cancel(); + await _bufferedSub?.cancel(); + await _eventosSub?.cancel(); await _player.dispose(); }