fix(player): serialize live stream switching
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m24s
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s

This commit is contained in:
2026-05-21 00:50:17 +02:00
parent 26d8151d7a
commit 6b0faebc7f
4 changed files with 197 additions and 13 deletions
+96 -13
View File
@@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'package:audio_session/audio_session.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
@@ -90,9 +92,12 @@ class ServicioAudio {
// AudioHandler
// ─────────────────────────────────────────────────────────────────────────────
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
static const _timeoutCambioFuente = Duration(seconds: 12);
final AndroidEqualizer _eq = AndroidEqualizer();
late final AudioPlayer _player = AudioPlayer(
userAgent: 'PluriWave/0.1.0 (es.freetimelab.pluriwave)',
audioPipeline: AudioPipeline(androidAudioEffects: [_eq]),
);
@@ -107,10 +112,28 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
PresetEcualizador _presetActual = PresetEcualizador.flat;
PresetEcualizador get presetActual => _presetActual;
Future<void> _colaCambiosFuente = Future<void>.value();
int _revisionFuente = 0;
bool _cambiandoFuente = false;
PluriWaveAudioHandler() {
unawaited(_configurarSesionAudio());
_setupStreams();
}
Future<void> _configurarSesionAudio() async {
try {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.music());
} catch (e) {
developer.log(
'[PluriWave] No se pudo configurar AudioSession: $e',
name: 'ServicioAudio',
level: 800,
);
}
}
void _setupStreams() {
_player.playerStateStream.listen((state) {
final playing = state.playing;
@@ -136,6 +159,15 @@ 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);
},
);
@@ -214,33 +246,83 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
@override
Future<void> playMediaItem(MediaItem mediaItem) async {
final revision = ++_revisionFuente;
_colaCambiosFuente = _colaCambiosFuente
.catchError((_) {})
.then((_) => _cambiarFuente(mediaItem, revision));
return _colaCambiosFuente;
}
Future<void> _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();
await _player.setUrl(mediaItem.id);
await _player.play();
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
await _player.stop().timeout(_timeoutCambioFuente);
if (revision != _revisionFuente) return;
await _player
.setAudioSource(
AudioSource.uri(Uri.parse(mediaItem.id), tag: mediaItem),
preload: false,
)
.timeout(_timeoutCambioFuente);
if (revision != _revisionFuente) return;
await _activarEcualizador();
_reproducirSinBloquear(mediaItem, revision);
} 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;
} 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<void> _activarEcualizador() async {
try {
final params = await _eq.parameters;
@@ -295,6 +377,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
@override
Future<void> stop() async {
_revisionFuente++;
await _player.stop();
emisoraActual = null;
mediaItem.add(null);