feat(mvp): PluriWave Fase 1 — estructura completa de la app
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
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
This commit is contained in:
175
lib/servicios/servicio_audio.dart
Normal file
175
lib/servicios/servicio_audio.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user