import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'sleep_timer.dart'; /// Kern der Wiedergabe: kapselt just_audio hinter audio_service, /// damit Hintergrund-Wiedergabe + Lockscreen/Notification funktionieren. class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final AudioPlayer _player = AudioPlayer(); late final SleepTimer sleepTimer; MeloAudioHandler() { sleepTimer = SleepTimer(onElapsed: pause); // just_audio-Events → audio_service PlaybackState _player.playbackEventStream.map(_transformEvent).pipe(playbackState); // Aktuellen Track ans System melden (Lockscreen/Notification). // Bereits bekannte Dauer erhalten, damit sie nicht auf null zurückfällt. _player.currentIndexStream.listen((index) { final q = queue.value; if (index != null && index < q.length) { mediaItem.add(q[index].copyWith(duration: _player.duration)); } }); // Ermittelte Dauer nachtragen _player.durationStream.listen((duration) { final item = mediaItem.value; if (item != null && duration != null) { mediaItem.add(item.copyWith(duration: duration)); } }); } /// Ersetzt die Warteschlange und startet ab [startIndex]. Future loadPlaylist(List items, {int startIndex = 0}) async { queue.add(items); final sources = items .map((item) => AudioSource.uri(Uri.parse(item.id), tag: item)) .toList(); await _player.setAudioSources(sources, initialIndex: startIndex); await play(); } @override Future play() => _player.play(); @override Future pause() => _player.pause(); @override Future stop() async { sleepTimer.cancel(); await _player.stop(); await super.stop(); } @override Future seek(Duration position) => _player.seek(position); @override Future skipToNext() => _player.seekToNext(); @override Future skipToPrevious() => _player.seekToPrevious(); @override Future skipToQueueItem(int index) => _player.seek(Duration.zero, index: index); @override Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { final enabled = shuffleMode == AudioServiceShuffleMode.all; if (enabled) await _player.shuffle(); await _player.setShuffleModeEnabled(enabled); } @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) { return _player.setLoopMode(switch (repeatMode) { AudioServiceRepeatMode.one => LoopMode.one, AudioServiceRepeatMode.all => LoopMode.all, _ => LoopMode.off, }); } PlaybackState _transformEvent(PlaybackEvent event) { return PlaybackState( controls: [ MediaControl.skipToPrevious, if (_player.playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, ], systemActions: const { MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, }, androidCompactActionIndices: const [0, 1, 2], processingState: const { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, ProcessingState.buffering: AudioProcessingState.buffering, ProcessingState.ready: AudioProcessingState.ready, ProcessingState.completed: AudioProcessingState.completed, }[_player.processingState] ?? AudioProcessingState.idle, playing: _player.playing, updatePosition: _player.position, bufferedPosition: _player.bufferedPosition, speed: _player.speed, queueIndex: event.currentIndex, ); } // Streams, die die UI beobachtet. Dauer direkt vom Player — zuverlässiger // als mediaItem.duration, das beim Track-Wechsel kurzzeitig null sein kann. Stream get positionStream => _player.positionStream; Stream get durationStream => _player.durationStream; }