import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:just_audio/just_audio.dart'; /// Owns the [AudioPlayer] and exposes it to the OS (lock screen, notification, /// headset and Bluetooth buttons) through audio_service. class NkAudioHandler extends BaseAudioHandler with SeekHandler { NkAudioHandler() { _init(); } /// Sound settings (Android only; iOS has no per-app equalizer in just_audio). final equalizer = AndroidEqualizer(); final loudness = AndroidLoudnessEnhancer(); late final _player = AudioPlayer( audioPipeline: AudioPipeline(androidAudioEffects: [loudness, equalizer]), ); Stream get positionStream => _player.positionStream; Stream get durationStream => _player.durationStream; Future _init() async { final session = await AudioSession.instance; await session.configure(const AudioSessionConfiguration.music()); _player.playbackEventStream.listen( (_) => playbackState.add(_currentState()), onError: (Object e, StackTrace st) => playbackState.add( playbackState.value.copyWith(processingState: AudioProcessingState.error, errorMessage: '$e'), ), ); _player.playingStream.listen((_) => playbackState.add(_currentState())); _player.shuffleModeEnabledStream.listen((_) => playbackState.add(_currentState())); _player.loopModeStream.listen((_) => playbackState.add(_currentState())); _player.sequenceStateStream.listen((state) { final tag = state.currentSource?.tag; if (tag is MediaItem) mediaItem.add(tag); queue.add([ for (final s in state.effectiveSequence) if (s.tag is MediaItem) s.tag as MediaItem, ]); }); // Fill in the real duration once the file is loaded. _player.durationStream.listen((d) { final item = mediaItem.value; if (d != null && item != null && item.duration != d) mediaItem.add(item.copyWith(duration: d)); }); } /// Replaces the queue with [items] and starts playing at [index]. Future playQueue(List items, {int index = 0}) async { if (items.isEmpty) return; await _player.setAudioSources( [for (final item in items) _sourceFor(item)], initialIndex: index, ); await _player.play(); } /// Inserts [item] right after the current song. Future playNext(MediaItem item) async { final source = _sourceFor(item); final current = _player.currentIndex; if (current == null) return playQueue([item]); await _player.insertAudioSource(current + 1, source); } @override Future addQueueItem(MediaItem mediaItem) => _player.addAudioSource(_sourceFor(mediaItem)); /// Set at startup: returns the downloaded (encrypted) copy of a song, if there is one. AudioSource? Function(MediaItem item)? localSource; AudioSource _sourceFor(MediaItem item) => localSource?.call(item) ?? AudioSource.uri(Uri.parse(item.extras!['url'] as String), tag: item); @override Future play() => _player.play(); @override Future pause() => _player.pause(); @override Future seek(Duration position) => _player.seek(position); @override Future skipToNext() => _player.seekToNext(); /// Restarts the song when more than 3 s in, like every major player. @override Future skipToPrevious() async { if (_player.position > const Duration(seconds: 3) || !_player.hasPrevious) { return _player.seek(Duration.zero); } return _player.seekToPrevious(); } @override Future skipToQueueItem(int index) async { final indices = _player.shuffleModeEnabled ? _player.shuffleIndices : null; await _player.seek(Duration.zero, index: indices == null ? index : indices[index]); } @override Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { final enable = shuffleMode != AudioServiceShuffleMode.none; if (enable) await _player.shuffle(); await _player.setShuffleModeEnabled(enable); } @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) => _player.setLoopMode(switch (repeatMode) { AudioServiceRepeatMode.one => LoopMode.one, AudioServiceRepeatMode.all || AudioServiceRepeatMode.group => LoopMode.all, AudioServiceRepeatMode.none => LoopMode.off, }); @override Future stop() async { await _player.stop(); return super.stop(); } PlaybackState _currentState() { final playing = _player.playing; return PlaybackState( controls: [ MediaControl.skipToPrevious, if (playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, ], systemActions: const {MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward}, androidCompactActionIndices: const [0, 1, 2], processingState: switch (_player.processingState) { ProcessingState.idle => AudioProcessingState.idle, ProcessingState.loading => AudioProcessingState.loading, ProcessingState.buffering => AudioProcessingState.buffering, ProcessingState.ready => AudioProcessingState.ready, ProcessingState.completed => AudioProcessingState.completed, }, playing: playing, updatePosition: _player.position, bufferedPosition: _player.bufferedPosition, speed: _player.speed, queueIndex: _player.currentIndex, shuffleMode: _player.shuffleModeEnabled ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, repeatMode: switch (_player.loopMode) { LoopMode.one => AudioServiceRepeatMode.one, LoopMode.all => AudioServiceRepeatMode.all, LoopMode.off => AudioServiceRepeatMode.none, }, ); } }