Some checks failed
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
- Modelo Emisora: campos completos Radio Browser API (fromApi + fromMap) - ServicioRadio: cliente Radio Browser API (populares, tendencias, buscar por nombre/país/idioma/tag) - ServicioAudio: just_audio + audio_service wrapper (play/pause/stop/toggle, fade, background handler) - ServicioTimer: countdown con fade out gradual (15/30/60/90 min) - ServicioFavoritos: actualizado a v2 con campos codec/bitrate/votes/clickcount - EstadoRadio: ChangeNotifier global con Provider - PantallaInicio: grid emisoras populares, chips género, shimmer loading, pull-to-refresh - PantallaBuscar: SearchBar + filtros país/idioma, lista resultados - PantallaFavoritos: ReorderableListView + swipe-to-delete (Dismissible) - TarjetaEmisora: card + modo compacto ListTile, cached_network_image, shimmer fallback - MiniReproductor: barra inferior persistente con stream de estado - app.dart: MaterialApp + Provider + NavigationBar + timer dialog - main.dart: punto de entrada limpio - AndroidManifest.xml: permisos INTERNET + FOREGROUND_SERVICE + audio_service receivers
176 lines
5.2 KiB
Dart
176 lines
5.2 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import '../modelos/emisora.dart';
|
|
|
|
/// Estado de reproducción expuesto al UI.
|
|
enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error }
|
|
|
|
/// Wrapper sobre just_audio + audio_service para reproducción de radio en streaming.
|
|
///
|
|
/// ### Uso
|
|
/// ```dart
|
|
/// final servicio = ServicioAudio();
|
|
/// await servicio.inicializar();
|
|
/// await servicio.reproducir(emisora);
|
|
/// await servicio.pausar();
|
|
/// await servicio.detener();
|
|
/// ```
|
|
///
|
|
/// ### Background audio
|
|
/// Para habilitar reproducción en background, el handler [PluriWaveAudioHandler]
|
|
/// debe registrarse en main.dart con [AudioService.init]. Si no está registrado,
|
|
/// just_audio seguirá funcionando en foreground.
|
|
class ServicioAudio {
|
|
final AudioPlayer _player = AudioPlayer();
|
|
Emisora? _emisoraActual;
|
|
|
|
EstadoReproduccion _estado = EstadoReproduccion.detenido;
|
|
EstadoReproduccion get estado => _estado;
|
|
Emisora? get emisoraActual => _emisoraActual;
|
|
|
|
/// Stream de cambios de estado para el UI.
|
|
Stream<EstadoReproduccion> get estadoStream => _player.playerStateStream.map(
|
|
(s) {
|
|
if (s.processingState == ProcessingState.loading ||
|
|
s.processingState == ProcessingState.buffering) {
|
|
return EstadoReproduccion.cargando;
|
|
}
|
|
if (s.playing) return EstadoReproduccion.reproduciendo;
|
|
if (s.processingState == ProcessingState.idle) return EstadoReproduccion.detenido;
|
|
return EstadoReproduccion.pausado;
|
|
},
|
|
);
|
|
|
|
/// Inicia la reproducción de la [emisora] indicada.
|
|
Future<void> reproducir(Emisora emisora) async {
|
|
try {
|
|
_estado = EstadoReproduccion.cargando;
|
|
|
|
// Si es la misma emisora, reanudar sin recargar
|
|
if (_emisoraActual?.uuid == emisora.uuid && _player.audioSource != null) {
|
|
await _player.play();
|
|
_estado = EstadoReproduccion.reproduciendo;
|
|
return;
|
|
}
|
|
|
|
_emisoraActual = emisora;
|
|
await _player.stop();
|
|
await _player.setUrl(emisora.url);
|
|
await _player.play();
|
|
_estado = EstadoReproduccion.reproduciendo;
|
|
} on PlayerException catch (_) {
|
|
_estado = EstadoReproduccion.error;
|
|
rethrow;
|
|
} catch (e) {
|
|
_estado = EstadoReproduccion.error;
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Pausa la reproducción actual.
|
|
Future<void> pausar() async {
|
|
await _player.pause();
|
|
_estado = EstadoReproduccion.pausado;
|
|
}
|
|
|
|
/// Reanuda si estaba pausado.
|
|
Future<void> reanudar() async {
|
|
if (_player.audioSource != null) {
|
|
await _player.play();
|
|
_estado = EstadoReproduccion.reproduciendo;
|
|
}
|
|
}
|
|
|
|
/// Alterna entre pausa y reproducción.
|
|
Future<void> togglePlay() async {
|
|
if (_player.playing) {
|
|
await pausar();
|
|
} else {
|
|
await reanudar();
|
|
}
|
|
}
|
|
|
|
/// Detiene la reproducción y libera la fuente.
|
|
Future<void> detener() async {
|
|
await _player.stop();
|
|
_emisoraActual = null;
|
|
_estado = EstadoReproduccion.detenido;
|
|
}
|
|
|
|
/// Ajusta el volumen (0.0 - 1.0).
|
|
Future<void> setVolumen(double volumen) async {
|
|
await _player.setVolume(volumen.clamp(0.0, 1.0));
|
|
}
|
|
|
|
double get volumen => _player.volume;
|
|
bool get estaSonando => _player.playing;
|
|
|
|
/// Libera recursos. Llamar al destruir la pantalla raíz.
|
|
Future<void> dispose() async {
|
|
await _player.dispose();
|
|
}
|
|
}
|
|
|
|
/// Handler de audio_service para reproducción en background con notificación.
|
|
///
|
|
/// Registrar en main.dart:
|
|
/// ```dart
|
|
/// final handler = await AudioService.init(
|
|
/// builder: () => PluriWaveAudioHandler(),
|
|
/// config: const AudioServiceConfig(
|
|
/// androidNotificationChannelId: 'es.freetimelab.pluriwave.audio',
|
|
/// androidNotificationChannelName: 'PluriWave Radio',
|
|
/// androidNotificationOngoing: true,
|
|
/// androidStopForegroundOnPause: true,
|
|
/// ),
|
|
/// );
|
|
/// ```
|
|
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
|
final AudioPlayer _player = AudioPlayer();
|
|
|
|
PluriWaveAudioHandler() {
|
|
_player.playerStateStream.listen((state) {
|
|
final playing = state.playing;
|
|
final proc = state.processingState;
|
|
playbackState.add(playbackState.value.copyWith(
|
|
controls: [
|
|
if (playing) MediaControl.pause else MediaControl.play,
|
|
MediaControl.stop,
|
|
],
|
|
systemActions: const {MediaAction.seek},
|
|
androidCompactActionIndices: const [0],
|
|
processingState: {
|
|
ProcessingState.idle: AudioProcessingState.idle,
|
|
ProcessingState.loading: AudioProcessingState.loading,
|
|
ProcessingState.buffering: AudioProcessingState.buffering,
|
|
ProcessingState.ready: AudioProcessingState.ready,
|
|
ProcessingState.completed: AudioProcessingState.completed,
|
|
}[proc]!,
|
|
playing: playing,
|
|
));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> playMediaItem(MediaItem item) async {
|
|
mediaItem.add(item);
|
|
await _player.setUrl(item.id);
|
|
await _player.play();
|
|
}
|
|
|
|
@override
|
|
Future<void> play() => _player.play();
|
|
|
|
@override
|
|
Future<void> pause() => _player.pause();
|
|
|
|
@override
|
|
Future<void> stop() async {
|
|
await _player.stop();
|
|
await super.stop();
|
|
}
|
|
|
|
@override
|
|
Future<void> seek(Duration position) => _player.seek(position);
|
|
}
|