import 'dart:async'; import 'dart:developer'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:rxdart/rxdart.dart'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import '../local_storage/key_value_storage.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../type/episodebrief.dart'; import '../type/play_histroy.dart'; import '../type/playlist.dart'; MediaControl playControl = MediaControl( androidIcon: 'drawable/ic_stat_play_circle_filled', label: 'Play', action: MediaAction.play, ); MediaControl pauseControl = MediaControl( androidIcon: 'drawable/ic_stat_pause_circle_filled', label: 'Pause', action: MediaAction.pause, ); MediaControl skipToNextControl = MediaControl( androidIcon: 'drawable/baseline_skip_next_white_24', label: 'Next', action: MediaAction.skipToNext, ); MediaControl skipToPreviousControl = MediaControl( androidIcon: 'drawable/ic_action_skip_previous', label: 'Previous', action: MediaAction.skipToPrevious, ); MediaControl stopControl = MediaControl( androidIcon: 'drawable/baseline_close_white_24', label: 'Stop', action: MediaAction.stop, ); MediaControl forwardControl = MediaControl( androidIcon: 'drawable/baseline_fast_forward_white_24', label: 'forward', action: MediaAction.fastForward, ); MediaControl rewindControl = MediaControl( androidIcon: 'drawable/baseline_fast_rewind_white_24', label: 'rewind', action: MediaAction.rewind, ); /// Sleep timer mode. enum SleepTimerMode { endOfEpisode, timer, undefined } enum PlayerHeight { short, mid, tall } class AudioPlayerNotifier extends ChangeNotifier { final _dbHelper = DBHelper(); final _positionStorage = KeyValueStorage(audioPositionKey); final _autoPlayStorage = KeyValueStorage(autoPlayKey); final _autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey); final _defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey); final _autoSleepTimerModeStorage = KeyValueStorage(autoSleepTimerModeKey); final _autoSleepTimerStartStorage = KeyValueStorage(autoSleepTimerStartKey); final _autoSleepTimerEndStorage = KeyValueStorage(autoSleepTimerEndKey); final _fastForwardSecondsStorage = KeyValueStorage(fastForwardSecondsKey); final _rewindSecondsStorage = KeyValueStorage(rewindSecondsKey); final _playerHeightStorage = KeyValueStorage(playerHeightKey); final _speedStorage = KeyValueStorage(speedKey); final _skipSilenceStorage = KeyValueStorage(skipSilenceKey); final _boostVolumeStorage = KeyValueStorage(boostVolumeKey); final _volumeGainStorage = KeyValueStorage(volumeGainKey); final _markListenedAfterSkipStorage = KeyValueStorage(markListenedAfterSkipKey); final _playlistsStorgae = KeyValueStorage(playlistsAllKey); final _playerStateStorage = KeyValueStorage(playerStateKey); final cacheStorage = KeyValueStorage(cacheMaxKey); /// Playing episdoe. EpisodeBrief? _episode; /// Playlists include queue and playlists created by user. List _playlists = []; /// Playing playlist. Playlist? _playlist; /// Queue is the first playlist by default. Playlist get _queue => _playlists.first; /// Player state. AudioProcessingState _audioState = AudioProcessingState.loading; /// Player playing. bool _playing = false; /// Fastforward second. int? _fastForwardSeconds = 0; /// Rewind seconds. int? _rewindSeconds = 0; /// No slide, set true if slide on seekbar. bool _noSlide = true; /// Current episode duration. int _backgroundAudioDuration = 0; /// Current episode positin. int? _backgroundAudioPosition = 0; /// Erroe maeesage. String? _remoteErrorMessage; /// Seekbar value, min 0, max 1.0. double _seekSliderValue = 0.0; /// Record plyaer position. int _lastPosition = 0; /// Set true if sleep timer mode is end of episode. bool _stopOnComplete = false; /// Sleep timer timer. late Timer _stopTimer; /// Sleep timer time left. int _timeLeft = 0; /// Start sleep timer. bool _startSleepTimer = false; /// Control sleep timer anamation. double _switchValue = 0; /// Sleep timer mode. SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined; //Auto stop at the end of episode when you start play at scheduled time. bool? _autoSleepTimer; //set autoplay episode in playlist late bool _autoPlay; /// Datetime now. DateTime? _current; /// Current position. late int _currentPosition; /// Current speed. double? _currentSpeed = 1; ///Update episode card when setting changed bool _episodeState = false; /// Player height. PlayerHeight? _playerHeight; /// Player skip silence. bool? _skipSilence; /// Boost volumn bool? _boostVolume; /// Boost volume gain. int? _volumeGain; // ignore: prefer_final_fields bool _playerRunning = false; late bool _markListened; // Tmep episode list, playing from search result List _playFromSearchList = []; late AudioHandler _audioHandler; StreamSubscription? _mediaitemSubscription; StreamSubscription? _playbackStateSubscription; StreamSubscription? _customEventSubscription; @override void addListener(VoidCallback listener) async { await _initAudioData(); final cacheMax = await cacheStorage.getInt(defaultValue: (1024 * 1024 * 200).toInt()); _audioHandler = await AudioService.init( builder: () => CustomAudioHandler(cacheMax), config: _config); super.addListener(listener); } @override void dispose() { _mediaitemSubscription?.cancel(); _playbackStateSubscription?.cancel(); _customEventSubscription?.cancel(); super.dispose(); } /// Audio service config AudioServiceConfig get _config => AudioServiceConfig( androidNotificationChannelName: 'Tsacdop', androidNotificationIcon: 'drawable/ic_notification', // androidEnableQueue: true, androidStopForegroundOnPause: true, preloadArtwork: false, fastForwardInterval: Duration(seconds: _fastForwardSeconds!), rewindInterval: Duration(seconds: _rewindSeconds!), ); /// Audio playing state. AudioProcessingState get audioState => _audioState; int get backgroundAudioDuration => _backgroundAudioDuration; int? get backgroundAudioPosition => _backgroundAudioPosition; double get seekSliderValue => _seekSliderValue; String? get remoteErrorMessage => _remoteErrorMessage; bool get playerRunning => _playerRunning; bool get buffering => _audioState != AudioProcessingState.ready; EpisodeBrief? get episode => _episode; /// Playlist provider. int get lastPosition => _lastPosition; List get playlists => _playlists; Playlist get queue => _playlists.first; bool get playing => _playing; Playlist? get playlist => _playlist; /// Player control. bool get stopOnComplete => _stopOnComplete; bool get startSleepTimer => _startSleepTimer; SleepTimerMode get sleepTimerMode => _sleepTimerMode; int get timeLeft => _timeLeft; double get switchValue => _switchValue; double? get currentSpeed => _currentSpeed; bool get episodeState => _episodeState; bool? get autoSleepTimer => _autoSleepTimer; int? get fastForwardSeconds => _fastForwardSeconds; int? get rewindSeconds => _rewindSeconds; PlayerHeight? get playerHeight => _playerHeight; bool? get skipSilence => _skipSilence; bool? get boostVolume => _boostVolume; int? get volumeGain => _volumeGain; set setSwitchValue(double value) { _switchValue = value; notifyListeners(); } set setEpisodeState(bool boo) { _episodeState = !_episodeState; notifyListeners(); } set setPlayerHeight(PlayerHeight mode) { _playerHeight = mode; notifyListeners(); _savePlayerHeight(); } Future _initAudioData() async { var index = await _playerHeightStorage.getInt(defaultValue: 0); _playerHeight = PlayerHeight.values[index]; _currentSpeed = await _speedStorage.getDouble(defaultValue: 1.0); _skipSilence = await _skipSilenceStorage.getBool(defaultValue: false); _boostVolume = await _boostVolumeStorage.getBool(defaultValue: false); _volumeGain = await _volumeGainStorage.getInt(defaultValue: 3000); _fastForwardSeconds = await _fastForwardSecondsStorage.getInt(defaultValue: 30); _rewindSeconds = await _rewindSecondsStorage.getInt(defaultValue: 30); } Future _savePlayerHeight() async { await _playerHeightStorage.saveInt(_playerHeight!.index); } Future _getAutoPlay() async { var i = await _autoPlayStorage.getInt(); _autoPlay = i == 0; } Future _getAutoSleepTimer() async { var i = await _autoSleepTimerStorage.getInt(); _autoSleepTimer = i == 1; } Future initPlaylist() async { if (_playlists.isEmpty) { var playlistEntities = await _playlistsStorgae.getPlaylists(); _playlists = [ for (var entity in playlistEntities!) Playlist.fromEntity(entity) ]; await _playlists.first.getPlaylist(); await _getAutoPlay(); ///Get playerstate saved in storage. final state = await _playerStateStorage.getPlayerState(); final idList = [for (var p in _playlists) p.id]; if (idList.contains(state[0])) { _playlist = _playlists.firstWhere( (p) => p.id == state[0], ); await _playlist!.getPlaylist(); if (state[1] != '') { var episode = await _dbHelper.getRssItemWithUrl(state[1]); if (episode != null && ((!_playlist!.isQueue && _playlist!.contains(episode)) || (_playlist!.isQueue && _queue.isNotEmpty && _queue.episodes.first!.title == episode.title))) { _episode = episode; _lastPosition = int.parse(state[2]); if (_lastPosition > 0) { { final duration = episode.duration! * 1000; final seekValue = duration != 0 ? _lastPosition / duration : 1.0; final history = PlayHistory(episode.title, episode.enclosureUrl, _lastPosition ~/ 1000, seekValue); await _dbHelper.saveHistory(history); } } } else { _episode = _playlist!.isNotEmpty ? _playlist!.episodes.first : null; _lastPosition = 0; } } else { _episode = _playlist!.isNotEmpty ? _playlist!.episodes.first : null; _lastPosition = 0; } } else { _playlist = _playlists.first; _episode = _playlist!.isNotEmpty ? _playlist!.episodes.first : null; _lastPosition = 0; } notifyListeners(); await KeyValueStorage(lastWorkKey).saveInt(0); } } Future playFromLastPosition() async { if (_playlist!.episodes.isNotEmpty) { await _playlist!.getPlaylist(); if (_episode == null || !_playlist!.episodes.contains(_episode)) { _episode = _playlist!.isNotEmpty ? _playlist!.episodes.first : null; } _audioState = AudioProcessingState.loading; _backgroundAudioDuration = 0; _backgroundAudioPosition = 0; _seekSliderValue = 0; _playerRunning = true; notifyListeners(); _startAudioService(_playlist!, position: _lastPosition, index: _playlist!.episodes.indexOf(_episode)); } else { log('Playlist is empty'); } } Future playlistLoad(Playlist playlist) async { var p = playlist; if (playlist.name != 'Queue') { await updatePlaylist(p, updateEpisodes: true); } _playlist = p; notifyListeners(); if (playlist.isNotEmpty) { if (playerRunning) { _audioHandler.customAction('setIsQueue', {'isQueue': playlist.isQueue}); _audioHandler.customAction('changeQueue', { 'queue': [for (var e in p.episodes) e!.toMediaItem()] }); } else { _backgroundAudioDuration = 0; _backgroundAudioPosition = 0; _seekSliderValue = 0; _episode = playlist.episodes.first; _audioState = AudioProcessingState.loading; _playerRunning = true; notifyListeners(); _startAudioService(_playlist!, position: 0, index: 0); } } } Future episodeLoad(EpisodeBrief? episode, {int startPosition = 0, bool fromSearch = false}) async { var episodeNew; if (fromSearch) { episodeNew = episode; _playFromSearchList.add(episode); } else { episodeNew = await _dbHelper.getRssItemWithUrl(episode!.enclosureUrl); } // @TODO load episode from last position when player running if (playerRunning) { if (_playFromSearchList.contains(_episode)) { _queue.delFromPlaylist(_episode); } else { final history = PlayHistory(_episode!.title, _episode!.enclosureUrl, backgroundAudioPosition! ~/ 1000, seekSliderValue); await _dbHelper.saveHistory(history); } _queue.addToPlayListAt(episodeNew, 0); await updatePlaylist(_queue, updateEpisodes: !fromSearch); if (_playlist!.isQueue) { _audioHandler.customAction('setIsQueue', {'isQueue': true}); _audioHandler.customAction('changeQueue', { 'queue': [for (var e in _queue.episodes) e!.toMediaItem()] }); _playlist = _queue; } await _audioHandler.customAction('addQueueItemAt', {'mediaItem': episodeNew.toMediaItem(), 'index': 0}); if (startPosition > 0) { await _audioHandler.seek(Duration(milliseconds: startPosition)); } _remoteErrorMessage = null; notifyListeners(); if (episodeNew.isNew == 1) { await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl); } } else { await _queue.getPlaylist(); _queue.addToPlayListAt(episodeNew, 0); updatePlaylist(_queue, updateEpisodes: false); _backgroundAudioDuration = 0; _backgroundAudioPosition = 0; _seekSliderValue = 0; _episode = episodeNew; _playerRunning = true; _playlist = _queue; notifyListeners(); _startAudioService(_queue, position: startPosition); } } Future loadEpisodeFromPlaylist(EpisodeBrief? episode) async { if (_playlist!.episodes.contains(episode)) { var index = _playlist!.episodes.indexOf(episode); await _audioHandler.customAction('changeIndex', {'index': index}); } } Future _startAudioService(Playlist playlist, {int index = 0, int position = 0}) async { _stopOnComplete = false; _sleepTimerMode = SleepTimerMode.undefined; _switchValue = 0; /// Get fastword and rewind seconds. _fastForwardSeconds = await _fastForwardSecondsStorage.getInt(defaultValue: 30); _rewindSeconds = await _rewindSecondsStorage.getInt(defaultValue: 10); /// Get if auto mark listened after skip _markListened = await _markListenedAfterSkipStorage.getBool(defaultValue: false); /// Start audio service. //Check autoplay setting, if true only add one episode, else add playlist. await _getAutoPlay(); if (_autoPlay) { await _audioHandler.addQueueItems( ([for (var episode in playlist.episodes) episode!.toMediaItem()])); } else { await _audioHandler .addQueueItems([playlist.episodes[index]!.toMediaItem()]); } await _audioHandler.play(); //Check auto sleep timer setting await _getAutoSleepTimer(); if (_autoSleepTimer!) { var startTime = await (_autoSleepTimerStartStorage.getInt( defaultValue: 1380) as FutureOr); var endTime = await (_autoSleepTimerEndStorage.getInt(defaultValue: 360) as FutureOr); var currentTime = DateTime.now().hour * 60 + DateTime.now().minute; if ((startTime > endTime && (currentTime > startTime || currentTime < endTime)) || ((startTime < endTime) && (currentTime > startTime && currentTime < endTime))) { var mode = await (_autoSleepTimerModeStorage.getInt() as FutureOr); _sleepTimerMode = SleepTimerMode.values[mode]; var defaultTimer = await _defaultSleepTimerStorage.getInt(defaultValue: 30); sleepTimer(defaultTimer); } } /// Set if playlist is queue. await _audioHandler .customAction('setIsQueue', {'isQueue': playlist.isQueue}); /// Set player speed. if (_currentSpeed != 1.0) { await _audioHandler.customAction('setSpeed', {'speed': _currentSpeed}); } /// Set slipsilence. if (_skipSilence!) { await _audioHandler .customAction('setSkipSilence', {'skipSilence': skipSilence}); } /// Set boostValome. if (_boostVolume!) { await _audioHandler.customAction( 'setBoostVolume', {'boostVolume': _boostVolume, 'gain': _volumeGain}); } _audioHandler.play(); _mediaitemSubscription = _audioHandler.mediaItem.where((event) => event != null).listen( (item) async { var episode = await _dbHelper.getRssItemWithMediaId(item!.id); if (episode == null) { episode = _playFromSearchList.firstWhere((e) => e!.mediaId == item.id, orElse: () => null); } if (episode != null) { _episode = episode; _backgroundAudioDuration = item.duration?.inMilliseconds ?? 0; if (position > 0 && _backgroundAudioDuration > 0 && _episode!.enclosureUrl == _playlist!.episodeList[index]) { await _audioHandler.seek(Duration(milliseconds: position)); position = 0; } notifyListeners(); } else { _audioHandler.skipToNext(); } }, ); _playbackStateSubscription = _audioHandler.playbackState.listen((event) async { _current = DateTime.now(); _audioState = event.processingState; _playing = event.playing; _currentSpeed = event.speed; _currentPosition = event.updatePosition.inMilliseconds; if (_audioState == AudioProcessingState.completed) { if (_switchValue > 0) _switchValue = 0; } /// Get error state. if (_audioState == AudioProcessingState.error) { _remoteErrorMessage = 'Network Error'; } /// Reset error state. if (_audioState != AudioProcessingState.error) { _remoteErrorMessage = null; } notifyListeners(); }); _customEventSubscription = _audioHandler.customEvent.distinct().listen((event) async { if (event is Map && event['removePlayed'] != null) { log(event.toString()); log(_queue.episodes.first!.title!); if (_playlist!.isQueue && _queue.isNotEmpty && _queue.episodes.first!.title == event['removePlayed']) { log(event['removePlayed']); _queue.delFromPlaylist(_episode); updatePlaylist(_queue, updateEpisodes: false); } _lastPosition = 0; notifyListeners(); // await _positionStorage.saveInt(_lastPostion); await _playerStateStorage.savePlayerState( _playlist!.id, _episode!.enclosureUrl, _lastPosition); var history; if (_markListened) { history = PlayHistory(_episode!.title, _episode!.enclosureUrl, _backgroundAudioPosition! ~/ 1000, 1); } else { history = PlayHistory(_episode!.title, _episode!.enclosureUrl, _backgroundAudioPosition! ~/ 1000, _seekSliderValue); } await _dbHelper.saveHistory(history); } if (event is Map && event['playerRunning'] == false && _playerRunning) { _playerRunning = false; notifyListeners(); if (_lastPosition > 0) { final history = PlayHistory(_episode!.title, _episode!.enclosureUrl, _lastPosition ~/ 1000, _seekSliderValue); await _dbHelper.saveHistory(history); } if (_playlist!.isEmpty) { _episode = null; _backgroundAudioDuration = 0; _backgroundAudioPosition = 0; } } if (event is Map && event['position'] != null) { _backgroundAudioPosition = event['position'].inMilliseconds; if (_backgroundAudioDuration != 0 && _backgroundAudioPosition != null) { _seekSliderValue = _backgroundAudioPosition! / _backgroundAudioDuration; } else { _seekSliderValue = 0; } notifyListeners(); } }); } /// Queue management Future addToPlaylist(EpisodeBrief episode) async { var episodeNew = await (_dbHelper.getRssItemWithUrl(episode.enclosureUrl) as FutureOr); if (episodeNew.isNew == 1) { await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl); } if (!_queue.episodes.contains(episodeNew)) { if (playerRunning && _playlist!.isQueue) { await _audioHandler.addQueueItem(episodeNew.toMediaItem()); } if (_playlist!.isQueue && _queue.isEmpty) _episode = episodeNew; _queue.addToPlayList(episodeNew); await updatePlaylist(_queue, updateEpisodes: false); } } Future addToPlaylistAt(EpisodeBrief episode, int index) async { var episodeNew = await (_dbHelper.getRssItemWithUrl(episode.enclosureUrl) as FutureOr); if (episodeNew.isNew == 1) { await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl); } if (_playerRunning && _playlist!.isQueue) { await _audioHandler.customAction('addQueueItemAt', {'mediaItem': episodeNew.toMediaItem(), 'index': index}); } _queue.addToPlayListAt(episodeNew, index); await updatePlaylist(_queue, updateEpisodes: false); } Future addNewEpisode(List group) async { var newEpisodes = []; if (group.isEmpty) { newEpisodes = await _dbHelper.getRecentNewRssItem(); } else { newEpisodes = await _dbHelper.getGroupNewRssItem(group); } if (newEpisodes.length > 0 && newEpisodes.length < 100) { for (var episode in newEpisodes) { await addToPlaylist(episode); } } if (group.isEmpty) { await _dbHelper.removeAllNewMark(); } else { await _dbHelper.removeGroupNewMark(group); } } Future updateMediaItem(EpisodeBrief episode) async { if (episode.enclosureUrl == episode.mediaId && _episode != episode && _playlist!.contains(episode)) { var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl); _playlist!.updateEpisode(episodeNew); if (_playerRunning) { await _audioHandler.updateMediaItem(episodeNew!.toMediaItem()); } } } Future delFromPlaylist(EpisodeBrief episode) async { var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl); if (playerRunning && _playlist!.isQueue) { await _audioHandler.removeQueueItem(episodeNew!.toMediaItem()); } var index = _queue.delFromPlaylist(episodeNew); if (index == 0) { _lastPosition = 0; await _positionStorage.saveInt(0); } updatePlaylist(_queue, updateEpisodes: false); return index; } Future reorderPlaylist(int oldIndex, int newIndex) async { if (newIndex > oldIndex) { newIndex -= 1; } var episode = _queue.episodes[oldIndex]!; _queue.addToPlayListAt(episode, newIndex); updatePlaylist(_queue, updateEpisodes: false); if (playerRunning && _playlist!.name == 'Queue') { await _audioHandler.removeQueueItem(episode.toMediaItem()); await _audioHandler.customAction('addQueueItemAt', {'mediaItem': episode.toMediaItem(), 'index': newIndex}); } if (newIndex == 0) { _lastPosition = 0; await _positionStorage.saveInt(0); } } Future moveToTop(EpisodeBrief episode) async { await delFromPlaylist(episode); final episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl); if (_playerRunning && _playlist!.isQueue) { await _audioHandler.customAction( '', {'mediaItem': episodeNew!.toMediaItem(), 'index': 1}); _queue.addToPlayListAt(episode, 1, existed: false); } else { _queue.addToPlayListAt(episode, 0, existed: false); if (_playlist!.isQueue) { _lastPosition = 0; _positionStorage.saveInt(_lastPosition); _episode = episodeNew; } } updatePlaylist(_queue, updateEpisodes: false); return true; } /// Custom playlist management. void addPlaylist(Playlist playlist) { _playlists = [..._playlists, playlist]; notifyListeners(); _savePlaylists(); } void deletePlaylist(Playlist playlist) { _playlists = [ for (var p in _playlists) if (p != playlist) p ]; if (_playlist == playlist && !_playerRunning) { _playlist = _queue; _episode = _playlist!.isNotEmpty ? _queue.episodes.first : null; } notifyListeners(); _savePlaylists(); if (playlist.isLocal!) { _dbHelper.deleteLocalEpisodes(playlist.episodeList); } } void addEpisodesToPlaylist(Playlist playlist, {required List episodes}) { for (var e in episodes) { playlist.addToPlayList(e); if (playerRunning && playlist == _playlist) { _audioHandler.addQueueItem(e.toMediaItem()); } } updatePlaylist(playlist, updateEpisodes: false); } void removeEpisodeFromPlaylist(Playlist playlist, {required List episodes}) { for (var e in episodes) { playlist.delFromPlaylist(e); if (playerRunning && playlist == _playlist) { _audioHandler.removeQueueItem(e.toMediaItem()); } } updatePlaylist(playlist, updateEpisodes: false); } void reorderEpisodesInPlaylist(Playlist playlist, {required int oldIndex, required int newIndex}) async { playlist.reorderPlaylist(oldIndex, newIndex); if (playerRunning && playlist == _playlist) { if (newIndex > oldIndex) { newIndex -= 1; } await _audioHandler.removeQueueItem(episode!.toMediaItem()); await _audioHandler.customAction('addQueueItemAt', {'mediaItem': episode!.toMediaItem(), 'index': newIndex}); } updatePlaylist(playlist, updateEpisodes: false); } void clearPlaylist(Playlist playlist) { if (_playerRunning && _playlist!.isQueue && playlist.isQueue) { for (var e in playlist.episodes) { if (e != _episode) { delFromPlaylist(e!); } } } else { playlist.clear(); if (_playlist!.isQueue) _episode = null; } updatePlaylist(playlist, updateEpisodes: false); } Future updatePlaylist(Playlist playlist, {bool updateEpisodes = true}) async { if (updateEpisodes) await playlist.getPlaylist(); _playlists = [for (var p in _playlists) p.id == playlist.id ? playlist : p]; if (_playlist!.id == playlist.id) { if (playlist.isQueue) { _playlist = _queue; } else if (!_playerRunning) { _playlist = _playlists.firstWhere((e) => e.id == _playlist!.id); } notifyListeners(); } await _savePlaylists(); } bool playlistExisted(String? name) { for (var p in _playlists) { if (p.name == name) return true; } return false; } // void _updateAllPlaylists() { // _playlists = [..._playlists]; // notifyListeners(); // _savePlaylists(); // } Future _savePlaylists() async { await _playlistsStorgae .savePlaylists([for (var p in _playlists) p.toEntity()]); } /// Audio control. Future pauseAduio() async { await _audioHandler.pause(); } Future resumeAudio() async { _remoteErrorMessage = null; notifyListeners(); if (_audioState != AudioProcessingState.loading) { _audioHandler.play(); } } Future playNext() async { _remoteErrorMessage = null; if (_playlist!.isQueue && _queue.isNotEmpty) { _queue.delFromPlaylist(_episode); updatePlaylist(_queue, updateEpisodes: false); } await _audioHandler.skipToNext(); notifyListeners(); } Future forwardAudio(int s) async { var pos = _backgroundAudioPosition! + s * 1000; await _audioHandler.seek(Duration(milliseconds: pos)); } Future fastForward() async { await _audioHandler.fastForward(); } Future rewind() async { await _audioHandler.rewind(); } Future seekTo(int position) async { if (_audioState != AudioProcessingState.loading) { await _audioHandler.seek(Duration(milliseconds: position)); } } Future sliderSeek(double val) async { if (_audioState != AudioProcessingState.loading) { _noSlide = false; _seekSliderValue = val; notifyListeners(); _currentPosition = (val * _backgroundAudioDuration).toInt(); await _audioHandler.seek(Duration(milliseconds: _currentPosition)); _noSlide = true; } } /// Set player speed. Future setSpeed(double speed) async { await _audioHandler.customAction('setSpeed', {'speed': speed}); _currentSpeed = speed; await _speedStorage.saveDouble(_currentSpeed!); notifyListeners(); } // Set skip silence. Future setSkipSilence({required bool skipSilence}) async { await _audioHandler .customAction('setSkipSilence', {'skipSilence': skipSilence}); _skipSilence = skipSilence; await _skipSilenceStorage.saveBool(_skipSilence); notifyListeners(); } set setVolumeGain(int volumeGain) { _volumeGain = volumeGain; if (_playerRunning && _boostVolume!) { setBoostVolume(boostVolume: _boostVolume, gain: _volumeGain); } notifyListeners(); _volumeGainStorage.saveInt(volumeGain); } Future setBoostVolume({required bool? boostVolume, int? gain}) async { await _audioHandler.customAction( 'setBoostVolume', {'boostVolume': boostVolume, 'gain': _volumeGain}); _boostVolume = boostVolume; notifyListeners(); await _boostVolumeStorage.saveBool(boostVolume); } //Set sleep timer void sleepTimer(int? mins) { if (_sleepTimerMode == SleepTimerMode.timer) { _startSleepTimer = true; _switchValue = 1; notifyListeners(); _timeLeft = mins! * 60; Timer.periodic(Duration(seconds: 1), (timer) { if (_timeLeft == 0) { timer.cancel(); notifyListeners(); } else { _timeLeft = _timeLeft - 1; notifyListeners(); } }); _stopTimer = Timer(Duration(minutes: mins), () { _stopOnComplete = false; _startSleepTimer = false; _switchValue = 0; if (_playerRunning) { _audioHandler.stop(); } notifyListeners(); // AudioService.disconnect(); }); } else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) { _stopOnComplete = true; _switchValue = 1; notifyListeners(); if (_queue.episodes.length > 1 && _autoPlay) { _audioHandler.customAction('stopAtEnd', {}); } } } set setSleepTimerMode(SleepTimerMode timer) { _sleepTimerMode = timer; notifyListeners(); } //Cancel sleep timer void cancelTimer() { if (_sleepTimerMode == SleepTimerMode.timer) { _stopTimer.cancel(); _timeLeft = 0; _startSleepTimer = false; _switchValue = 0; notifyListeners(); } else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) { _audioHandler.customAction('cancelStopAtEnd', {}); _switchValue = 0; _stopOnComplete = false; notifyListeners(); } } } class CustomAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final cacheStorage = KeyValueStorage(cacheMaxKey); final layoutStorage = KeyValueStorage(notificationLayoutKey); final _equalizer = AndroidEqualizer(); final _loudnessEnhancer = AndroidLoudnessEnhancer(); late final AudioPlayer _player = AudioPlayer( audioPipeline: AudioPipeline( androidAudioEffects: [ _loudnessEnhancer, _equalizer, ], ), ); bool _interrupted = false; int? _layoutIndex; bool _stopAtEnd = false; bool? _isQueue = false; bool _autoSkip = true; ConcatenatingAudioSource _playlist = ConcatenatingAudioSource( useLazyPreparation: true, shuffleOrder: DefaultShuffleOrder(), children: [], ); bool get hasNext => queue.value!.length > 0; MediaItem? get currentMediaItem => mediaItem.value; bool get playing => playbackState.value!.playing; PublishSubject> customEvent = PublishSubject()..add({}); CustomAudioHandler(int cacheMax) { // _player.cacheMax = cacheMax; _handleInterruption(); _player.currentIndexStream.listen( (index) { if (queue.value!.isNotEmpty && index! < queue.value!.length) { mediaItem.add(queue.value![index]); } if (_isQueue! && _autoSkip) { customEvent.add({'removePlayed': queue.value!.first.title}); } _autoSkip = true; }, ); _player.playbackEventStream.listen((event) async { if (_layoutIndex == null) { _layoutIndex = await layoutStorage.getInt(); } playbackState.add(playbackState.value!.copyWith( controls: _getControls(_layoutIndex), androidCompactActionIndices: [0, 1, 2], systemActions: { MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, }, processingState: { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, ProcessingState.buffering: AudioProcessingState.buffering, ProcessingState.ready: AudioProcessingState.ready, ProcessingState.completed: AudioProcessingState.completed, }[_player.processingState]!, playing: _player.playing, updatePosition: _player.position, queueIndex: _player.currentIndex ?? 0, bufferedPosition: _player.bufferedPosition, speed: _player.speed, )); }); _player.positionStream.listen((event) { customEvent.add({'position': event}); }); _player.sequenceStream.listen((event) { log(event.toString()); }); _player.durationStream.listen((event) { mediaItem.add(mediaItem.value!.copyWith(duration: _player.duration!)); }); } @override Future addQueueItems(List items) async { queue.add(items); _setAudioSource(items); _player.setAudioSource(_playlist); } void _setAudioSource(List items) { _playlist.insertAll( 0, [for (var item in items) _itemToSource(item)], ); } void _handleInterruption() async { final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration.speech()); session.interruptionEventStream.listen((event) { if (event.begin) { switch (event.type) { case AudioInterruptionType.pause: if (playing) { pause(); _interrupted = true; } break; case AudioInterruptionType.duck: if (playing) { pause(); _interrupted = true; } break; case AudioInterruptionType.unknown: if (playing) { pause(); _interrupted = true; } break; } } else { switch (event.type) { case AudioInterruptionType.pause: if (!playing && _interrupted) { play(); } break; case AudioInterruptionType.duck: if (!playing && _interrupted) { play(); } break; case AudioInterruptionType.unknown: break; } _interrupted = false; } }); session.becomingNoisyEventStream.listen((_) { if (playing) pause(); }); } void playPause() { if (playbackState.value!.playing) { pause(); } else { play(); } } @override Future skipToNext() async { if (queue.value!.length == 1 || _stopAtEnd) { await Future.delayed(Duration(milliseconds: 200)); await stop(); } else { _autoSkip = false; await super.skipToNext(); _player.seekToNext(); if (_isQueue!) { removeQueueItemAt(0); } } } @override Future play() async { if (!playing) { await super.play(); await _player.play(); } else { super.play(); await _player.play(); await _seekRelative(Duration(seconds: -3)); } } @override Future addQueueItem(MediaItem item) async { _addQueueItemAt(item, queue.value!.length); } @override Future removeQueueItemAt(int index) async { queue.add(queue.value!..removeAt(index)); _playlist.removeAt(index); super.removeQueueItemAt(index); } @override Future pause() async { await _player.pause(); } @override Future seek(Duration position) async { await _player.seek(position); super.seek(position); } Future fastForward() async { _seekRelative(AudioService.config.fastForwardInterval); } Future rewind() async { _seekRelative(-AudioService.config.rewindInterval); } Future onClick(MediaButton button) async { switch (button) { case MediaButton.media: if (playing) { await pause(); } else { await play(); } break; case MediaButton.next: await fastForward(); break; case MediaButton.previous: await rewind(); break; } } Future _seekRelative(Duration offset) async { var newPosition = playbackState.value!.position + offset; if (newPosition < Duration.zero) newPosition = Duration.zero; seek(newPosition); } Future stop() async { await _player.stop(); // await _player.dispose(); _playlist.clear(); customEvent.add({'playerRunning': false}); await super.stop(); } Future taskRemoved() async { await stop(); } Future _addQueueItemAt(MediaItem item, int index) async { log(index.toString() + ': ' + item.toString()); if (index == 0 && _isQueue!) { queue.add(queue.value!..removeWhere((i) => i.id == item.id)); queue.add(queue.value!..insert(index, item)); skipToNext(); } else { queue.add(queue.value!..insert(index, item)); } _playlist.insert(index, _itemToSource(item)); } @override Future customAction(function, [argument]) async { switch (function) { case 'stopAtEnd': _stopAtEnd = true; break; case 'cancelStopAtEnd': _stopAtEnd = false; break; case 'setSpeed': log('Argument' + argument!['speed'].toString()); await _player.setSpeed(argument['speed']); break; case 'setSkipSilence': await _setSkipSilence(argument!['skipSilence']); break; case 'setBoostVolume': await _setBoostVolume(argument!['boostVolume'], argument['gain']); break; case 'setIsQueue': log('Argument' + argument!['isQueue'].toString()); _isQueue = argument['isQueue']; break; case 'changeQueue': await _changeQueue(argument!['queue']); break; case 'changeIndex': await _changeIndex(argument!['index']); break; case 'addQueueItemAt': await _addQueueItemAt(argument!['mediaItem'], argument['index']); break; default: super.customAction(function, argument); } } Future _changeQueue(List newQueue) async { await _player.stop(); queue.add(newQueue); play(); } Future _changeIndex(int index) async { await super.skipToQueueItem(index); } Future _setSkipSilence(bool boo) async { await _player.setSkipSilenceEnabled(boo); } Future _setBoostVolume(bool enabled, int gain) async { await _loudnessEnhancer.setEnabled(enabled); await _loudnessEnhancer.setTargetGain(1.5); } List _getControls(int? index) { switch (index) { case 0: return [ playing ? pauseControl : playControl, forwardControl, skipToNextControl, stopControl ]; case 1: return [ playing ? pauseControl : playControl, rewindControl, skipToNextControl, stopControl ]; case 2: return [ rewindControl, playing ? pauseControl : playControl, forwardControl, stopControl ]; default: return [ playing ? pauseControl : playControl, forwardControl, skipToNextControl, stopControl ]; } } static AudioSource _itemToSource(MediaItem item) { return ClippingAudioSource( start: Duration(seconds: item.extras!['skipSecondsStart']), // end: Duration(seconds: item.extras['skipSecondsEnd']), child: AudioSource.uri(Uri.parse(item.id))); } }