diff --git a/docs/audio-switching-notes.md b/docs/audio-switching-notes.md index 4ad783e..5aa4049 100644 --- a/docs/audio-switching-notes.md +++ b/docs/audio-switching-notes.md @@ -4,22 +4,22 @@ Referencia interna para futuras correcciones del reproductor de PluriWave. Este ## Hallazgos útiles -- `AudioPlayer.play()` **no debe esperarse como si fuera “arrancar y terminar”** en un stream de radio en vivo. Según la documentación de `just_audio`, el `Future` de `play()` completa cuando la reproducción termina, se pausa o se detiene. En una radio en vivo puede quedar vivo hasta que otra acción lo interrumpa. -- Para streams en vivo, la UI debe depender de `playerStateStream`: `loading`/`buffering` para spinner, `ready + playing` para estado en directo. -- El ejemplo de radio de `just_audio` configura sesión de audio y escucha errores de playback. Conviene tratar el cambio de emisora como una operación transaccional: parar fuente anterior, asignar fuente nueva y arrancar sin bloquear el flujo principal. -- En `audio_service`, si se cambia de fuente desde `playMediaItem`, hay que evitar que errores tardíos de la fuente anterior limpien la emisora nueva. Es una carrera típica cuando se hace `stop()` y enseguida se carga otro stream. +- `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. ## Decisión aplicada en PluriWave -- Mantener `setUrl(...)` porque era la ruta que conectaba correctamente la primera emisora en esta app. -- Serializar cambios de emisora con una cola interna para que no se solapen `stop/setUrl/play`. -- Usar una revisión incremental para que solo la última solicitud pueda actualizar estado/errores. -- Ejecutar `play()` sin `await`, porque en radios en vivo su `Future` no representa “ya arrancó”, sino “terminó/pausó/detuvo”. -- Proteger `EstadoRadio.reproducir` con revisión para que una reproducción vieja que termine tarde no aplique presets/clicks encima de la emisora nueva. +- 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. +- Proteger `EstadoRadio.reproducir` con revisión para que una operación vieja no aplique presets/clicks encima de una nueva. -## Intento descartado +## Intentos descartados -- Se probó `setAudioSource(..., preload: false)` siguiendo una interpretación más transaccional del API, pero en PluriWave rompió incluso la primera conexión. Queda descartado salvo que se acompañe de logs nativos que justifiquen retomarlo. +- `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. ## Fuentes consultadas diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index d47c991..cf77318 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -46,6 +46,7 @@ class EstadoRadio extends ChangeNotifier { StreamSubscription? _suscripcionEstadoAudio; Future? _initFuture; int _revisionReproduccion = 0; + Emisora? _emisoraSeleccionada; // Errores de reproducción → SnackBar. final _errorController = StreamController.broadcast(); @@ -91,7 +92,7 @@ class EstadoRadio extends ChangeNotifier { String? get paisCercanoDetectado => _paisCercanoDetectado; String? get errorCercanas => _errorCercanas; String? get error => _errorCarga; - Emisora? get emisoraActual => audio.emisoraActual; + Emisora? get emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual; Stream get estadoStream => audio.estadoStream; PresetEcualizador get presetEcualizador => _presetActual; PresetEcualizador get presetPrincipalEcualizador => _presetPrincipal; @@ -305,6 +306,8 @@ class EstadoRadio extends ChangeNotifier { Future reproducir(Emisora emisora) async { final revision = ++_revisionReproduccion; + _emisoraSeleccionada = emisora; + notifyListeners(); try { await audio.reproducir(emisora); if (revision != _revisionReproduccion) return; @@ -318,6 +321,7 @@ class EstadoRadio extends ChangeNotifier { unawaited(timer.cancelar()); } final mensajeError = e.toString().replaceFirst('Exception: ', ''); + _emisoraSeleccionada = audio.emisoraActual; _errorController.add( mensajeError.isNotEmpty && mensajeError != 'Exception' ? mensajeError diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart index d2e5939..633801e 100644 --- a/lib/servicios/servicio_audio.dart +++ b/lib/servicios/servicio_audio.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:developer' as developer; import 'package:audio_service/audio_service.dart'; @@ -91,8 +90,6 @@ class ServicioAudio { // AudioHandler // ───────────────────────────────────────────────────────────────────────────── class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { - static const _timeoutCambioFuente = Duration(seconds: 12); - final AndroidEqualizer _eq = AndroidEqualizer(); late final AudioPlayer _player = AudioPlayer( @@ -110,10 +107,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { PresetEcualizador _presetActual = PresetEcualizador.flat; PresetEcualizador get presetActual => _presetActual; - Future _colaCambiosFuente = Future.value(); - int _revisionFuente = 0; - bool _cambiandoFuente = false; - PluriWaveAudioHandler() { _setupStreams(); } @@ -143,15 +136,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { _player.playbackEventStream.listen( (_) {}, onError: (Object error, StackTrace stackTrace) { - if (_cambiandoFuente) { - developer.log( - '[PluriWave] Error ignorado durante cambio de emisora: $error', - name: 'ServicioAudio', - level: 800, - stackTrace: stackTrace, - ); - return; - } _gestionarErrorReproduccion(error); }, ); @@ -230,78 +214,33 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future playMediaItem(MediaItem mediaItem) async { - final revision = ++_revisionFuente; - _colaCambiosFuente = _colaCambiosFuente - .catchError((_) {}) - .then((_) => _cambiarFuente(mediaItem, revision)); - return _colaCambiosFuente; - } - - Future _cambiarFuente(MediaItem mediaItem, int revision) async { - final emisora = _emisoraDesdeMediaItem(mediaItem); this.mediaItem.add(mediaItem); - emisoraActual = emisora; - playbackState.add(playbackState.value.copyWith( - processingState: AudioProcessingState.loading, - playing: false, - errorMessage: null, - )); - - _cambiandoFuente = true; try { - await _player.stop().timeout(_timeoutCambioFuente); - if (revision != _revisionFuente) return; - - await _player.setUrl(mediaItem.id).timeout(_timeoutCambioFuente); - if (revision != _revisionFuente) return; - + await _player.stop(); + await _player.setUrl(mediaItem.id); + await _player.play(); + emisoraActual = _emisoraDesdeMediaItem(mediaItem); await _activarEcualizador(); - _reproducirSinBloquear(mediaItem, revision); } on PlayerException catch (e) { - if (revision == _revisionFuente) { - _gestionarErrorReproduccion(e); - } + _gestionarErrorReproduccion(e); throw Exception(_mensajeAmigable(e)); - } on Exception catch (e, stackTrace) { + } on Exception catch (e) { developer.log( '[PluriWave] Error inesperado en playMediaItem: $e', name: 'ServicioAudio', level: 900, - stackTrace: stackTrace, ); - if (revision == _revisionFuente) { - playbackState.add(playbackState.value.copyWith( - processingState: AudioProcessingState.error, - playing: false, - errorMessage: 'Error inesperado al reproducir', - )); - emisoraActual = null; - this.mediaItem.add(null); - } + playbackState.add(playbackState.value.copyWith( + processingState: AudioProcessingState.error, + playing: false, + errorMessage: 'Error inesperado al reproducir', + )); + emisoraActual = null; + this.mediaItem.add(null); rethrow; - } finally { - if (revision == _revisionFuente) { - _cambiandoFuente = false; - } } } - void _reproducirSinBloquear(MediaItem mediaItem, int revision) { - unawaited( - _player.play().catchError((Object error, StackTrace stackTrace) { - developer.log( - '[PluriWave] Error al arrancar ${mediaItem.title}: $error', - name: 'ServicioAudio', - level: 900, - stackTrace: stackTrace, - ); - if (revision == _revisionFuente) { - _gestionarErrorReproduccion(error); - } - }), - ); - } - Future _activarEcualizador() async { try { final params = await _eq.parameters; @@ -356,7 +295,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future stop() async { - _revisionFuente++; await _player.stop(); emisoraActual = null; mediaItem.add(null);