2020-06-23 19:23:12 +00:00
import ' package:audio_service/audio_service.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:audio_session/audio_session.dart ' ;
2021-02-09 20:14:14 +00:00
import ' package:equalizer/equalizer.dart ' ;
2020-06-23 19:23:12 +00:00
import ' package:fluttertoast/fluttertoast.dart ' ;
2020-10-09 18:52:45 +00:00
import ' package:freezer/api/cache.dart ' ;
2020-06-23 19:23:12 +00:00
import ' package:freezer/api/deezer.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:freezer/ui/android_auto.dart ' ;
2020-06-23 19:23:12 +00:00
import ' package:just_audio/just_audio.dart ' ;
import ' package:connectivity/connectivity.dart ' ;
import ' package:path/path.dart ' as p ;
import ' package:path_provider/path_provider.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:freezer/translations.i18n.dart ' ;
2020-11-09 21:05:47 +00:00
import ' package:scrobblenaut/scrobblenaut.dart ' ;
2021-02-09 20:14:14 +00:00
import ' package:extended_math/extended_math.dart ' ;
2020-06-23 19:23:12 +00:00
import ' definitions.dart ' ;
import ' ../settings.dart ' ;
import ' dart:io ' ;
import ' dart:async ' ;
import ' dart:convert ' ;
2021-03-16 19:35:50 +00:00
import ' dart:math ' ;
2020-06-23 19:23:12 +00:00
PlayerHelper playerHelper = PlayerHelper ( ) ;
class PlayerHelper {
StreamSubscription _customEventSubscription ;
2020-10-09 18:52:45 +00:00
StreamSubscription _mediaItemSubscription ;
2020-06-23 19:23:12 +00:00
StreamSubscription _playbackStateStreamSubscription ;
QueueSource queueSource ;
2020-08-13 17:39:22 +00:00
LoopMode repeatType = LoopMode . off ;
2020-11-09 21:05:47 +00:00
Timer _timer ;
2021-02-09 20:14:14 +00:00
int audioSession ;
int _prevAudioSession ;
bool equalizerOpen = false ;
//Visualizer
StreamController _visualizerController = StreamController . broadcast ( ) ;
Stream get visualizerStream = > _visualizerController . stream ;
2020-06-23 19:23:12 +00:00
//Find queue index by id
2020-10-22 18:27:09 +00:00
int get queueIndex = > AudioService . queue = = null ? 0 : AudioService . queue . indexWhere ( ( mi ) = > mi . id = = AudioService . currentMediaItem ? . id ? ? ' Random string so it returns -1 ' ) ;
2020-06-23 19:23:12 +00:00
Future start ( ) async {
2020-10-11 20:06:29 +00:00
//Subscribe to custom events
2020-06-23 19:23:12 +00:00
_customEventSubscription = AudioService . customEventStream . listen ( ( event ) async {
if ( ! ( event is Map ) ) return ;
2021-02-09 20:14:14 +00:00
switch ( event [ ' action ' ] ) {
case ' onLoad ' :
//After audio_service is loaded, load queue, set quality
await settings . updateAudioServiceQuality ( ) ;
await AudioService . customAction ( ' load ' ) ;
2021-07-02 16:28:59 +00:00
await authorizeLastFM ( ) ;
2021-02-09 20:14:14 +00:00
break ;
case ' onRestore ' :
//Load queueSource from isolate
this . queueSource = QueueSource . fromJson ( event [ ' queueSource ' ] ) ;
repeatType = LoopMode . values [ event [ ' loopMode ' ] ] ;
break ;
case ' queueEnd ' :
//If last song is played, load more queue
this . queueSource = QueueSource . fromJson ( event [ ' queueSource ' ] ) ;
// onQueueEnd();
break ;
case ' screenAndroidAuto ' :
AndroidAuto androidAuto = AndroidAuto ( ) ;
List < MediaItem > data = await androidAuto . getScreen ( event [ ' id ' ] ) ;
await AudioService . customAction ( ' screenAndroidAuto ' , jsonEncode ( data ) ) ;
break ;
case ' tracksAndroidAuto ' :
AndroidAuto androidAuto = AndroidAuto ( ) ;
await androidAuto . playItem ( event [ ' id ' ] ) ;
break ;
case ' audioSession ' :
if ( ! settings . enableEqualizer ) break ;
//Save
_prevAudioSession = audioSession ;
audioSession = event [ ' id ' ] ;
if ( audioSession = = null )
break ;
//Open EQ
if ( ! equalizerOpen ) {
Equalizer . open ( event [ ' id ' ] ) ;
equalizerOpen = true ;
break ;
}
//Change session id
if ( _prevAudioSession ! = audioSession ) {
if ( _prevAudioSession ! = null ) Equalizer . removeAudioSessionId ( _prevAudioSession ) ;
Equalizer . setAudioSessionId ( audioSession ) ;
}
break ;
//Visualizer data
case ' visualizer ' :
_visualizerController . add ( event [ ' data ' ] ) ;
break ;
2020-09-18 17:36:41 +00:00
}
2021-02-09 20:14:14 +00:00
2020-06-23 19:23:12 +00:00
} ) ;
2020-10-09 18:52:45 +00:00
_mediaItemSubscription = AudioService . currentMediaItemStream . listen ( ( event ) {
2020-10-11 20:06:29 +00:00
if ( event = = null ) return ;
2021-02-09 20:14:14 +00:00
//Load more flow if index-1 song
if ( queueIndex = = AudioService . queue . length - 1 )
onQueueEnd ( ) ;
2020-10-09 18:52:45 +00:00
//Save queue
AudioService . customAction ( ' saveQueue ' ) ;
//Add to history
if ( cache . history = = null ) cache . history = [ ] ;
if ( cache . history . length > 0 & & cache . history . last . id = = event . id ) return ;
cache . history . add ( Track . fromMediaItem ( event ) ) ;
cache . save ( ) ;
} ) ;
2020-11-09 21:05:47 +00:00
//Logging listen timer
_timer = Timer . periodic ( Duration ( seconds: 2 ) , ( timer ) async {
if ( AudioService . currentMediaItem = = null | | ! AudioService . playbackState . playing ) return ;
if ( AudioService . playbackState . currentPosition . inSeconds > ( AudioService . currentMediaItem . duration . inSeconds * 0.75 ) ) {
if ( cache . loggedTrackId = = AudioService . currentMediaItem . id ) return ;
cache . loggedTrackId = AudioService . currentMediaItem . id ;
await cache . save ( ) ;
//Log to Deezer
if ( settings . logListen ) {
deezerAPI . logListen ( AudioService . currentMediaItem . id ) ;
}
}
} ) ;
2020-06-23 19:23:12 +00:00
//Start audio_service
2020-10-11 20:06:29 +00:00
await startService ( ) ;
2020-06-23 19:23:12 +00:00
}
2020-06-24 14:52:53 +00:00
Future startService ( ) async {
2020-10-11 20:06:29 +00:00
if ( AudioService . running & & AudioService . connected ) return ;
if ( ! AudioService . connected )
await AudioService . connect ( ) ;
if ( ! AudioService . running )
await AudioService . start (
backgroundTaskEntrypoint: backgroundTaskEntrypoint ,
androidEnableQueue: true ,
androidStopForegroundOnPause: false ,
androidNotificationOngoing: false ,
androidNotificationClickStartsActivity: true ,
androidNotificationChannelDescription: ' Freezer ' ,
androidNotificationChannelName: ' Freezer ' ,
androidNotificationIcon: ' drawable/ic_logo ' ,
2020-10-19 19:28:45 +00:00
params: { ' ignoreInterruptions ' : settings . ignoreInterruptions }
2020-10-11 20:06:29 +00:00
) ;
2020-06-23 19:23:12 +00:00
}
2020-11-09 21:05:47 +00:00
Future authorizeLastFM ( ) async {
if ( settings . lastFMUsername = = null | | settings . lastFMPassword = = null ) return ;
2021-07-02 16:28:59 +00:00
await AudioService . customAction ( " authorizeLastFM " , [ settings . lastFMUsername , settings . lastFMPassword ] ) ;
2020-11-09 21:05:47 +00:00
}
2020-08-13 17:39:22 +00:00
Future toggleShuffle ( ) async {
2020-10-15 18:37:36 +00:00
await AudioService . customAction ( ' shuffle ' ) ;
2020-08-13 17:39:22 +00:00
}
2020-06-23 19:23:12 +00:00
//Repeat toggle
Future changeRepeat ( ) async {
//Change to next repeat type
switch ( repeatType ) {
2020-08-13 17:39:22 +00:00
case LoopMode . one:
repeatType = LoopMode . off ; break ;
case LoopMode . all:
repeatType = LoopMode . one ; break ;
2020-06-23 19:23:12 +00:00
default :
2020-08-13 17:39:22 +00:00
repeatType = LoopMode . all ; break ;
2020-06-23 19:23:12 +00:00
}
//Set repeat type
2020-08-13 17:39:22 +00:00
await AudioService . customAction ( " repeatType " , LoopMode . values . indexOf ( repeatType ) ) ;
2020-06-23 19:23:12 +00:00
}
//Executed before exit
Future onExit ( ) async {
_customEventSubscription . cancel ( ) ;
_playbackStateStreamSubscription . cancel ( ) ;
2020-10-09 18:52:45 +00:00
_mediaItemSubscription . cancel ( ) ;
2020-06-23 19:23:12 +00:00
}
//Replace queue, play specified track id
Future _loadQueuePlay ( List < MediaItem > queue , String trackId ) async {
2020-06-24 14:52:53 +00:00
await startService ( ) ;
2020-06-23 19:23:12 +00:00
await settings . updateAudioServiceQuality ( ) ;
2020-11-28 21:32:17 +00:00
await AudioService . customAction ( ' setIndex ' , queue . indexWhere ( ( m ) = > m . id = = trackId ) ) ;
2020-06-23 19:23:12 +00:00
await AudioService . updateQueue ( queue ) ;
2020-11-28 21:32:17 +00:00
// if (queue[0].id != trackId)
// await AudioService.skipToQueueItem(trackId);
2020-10-19 19:28:45 +00:00
if ( ! AudioService . playbackState . playing )
AudioService . play ( ) ;
2020-06-23 19:23:12 +00:00
}
2020-09-18 17:36:41 +00:00
//Called when queue ends to load more tracks
Future onQueueEnd ( ) async {
//Flow
if ( queueSource = = null ) return ;
2020-10-19 19:28:45 +00:00
2020-10-24 21:00:29 +00:00
List < Track > tracks = [ ] ;
switch ( queueSource . source ) {
case ' flow ' :
tracks = await deezerAPI . flow ( ) ;
break ;
2021-02-09 20:14:14 +00:00
//SmartRadio/Artist radio
case ' smartradio ' :
2020-10-24 21:00:29 +00:00
tracks = await deezerAPI . smartRadio ( queueSource . id ) ;
break ;
2021-02-09 20:14:14 +00:00
//Library shuffle
case ' libraryshuffle ' :
2020-10-24 21:00:29 +00:00
tracks = await deezerAPI . libraryShuffle ( start: AudioService . queue . length ) ;
break ;
case ' mix ' :
tracks = await deezerAPI . playMix ( queueSource . id ) ;
// Deduplicate tracks with the same id
List < String > queueIds = AudioService . queue . map ( ( e ) = > e . id ) . toList ( ) ;
tracks . removeWhere ( ( track ) = > queueIds . contains ( track . id ) ) ;
break ;
default :
2021-02-09 20:14:14 +00:00
// print(queueSource.toJson());
break ;
2020-10-19 19:28:45 +00:00
}
2020-10-24 21:00:29 +00:00
List < MediaItem > mi = tracks . map < MediaItem > ( ( t ) = > t . toMediaItem ( ) ) . toList ( ) ;
await AudioService . addQueueItems ( mi ) ;
2021-02-09 20:14:14 +00:00
// AudioService.skipToNext();
2020-09-18 17:36:41 +00:00
}
2020-06-23 19:23:12 +00:00
//Play track from album
Future playFromAlbum ( Album album , String trackId ) async {
await playFromTrackList ( album . tracks , trackId , QueueSource (
id: album . id ,
text: album . title ,
source : ' album '
) ) ;
}
2020-10-24 21:00:29 +00:00
//Play mix by track
Future playMix ( String trackId , String trackTitle ) async {
List < Track > tracks = await deezerAPI . playMix ( trackId ) ;
playFromTrackList ( tracks , tracks [ 0 ] . id , QueueSource (
id: trackId ,
text: ' Mix based on ' . i18n + ' $ trackTitle ' ,
source : ' mix '
) ) ;
}
2020-06-23 19:23:12 +00:00
//Play from artist top tracks
Future playFromTopTracks ( List < Track > tracks , String trackId , Artist artist ) async {
await playFromTrackList ( tracks , trackId , QueueSource (
id: artist . id ,
text: ' Top ${ artist . name } ' ,
source : ' topTracks '
) ) ;
}
Future playFromPlaylist ( Playlist playlist , String trackId ) async {
await playFromTrackList ( playlist . tracks , trackId , QueueSource (
id: playlist . id ,
text: playlist . title ,
source : ' playlist '
) ) ;
}
2020-11-28 21:32:17 +00:00
//Play episode from show, load whole show as queue
Future playShowEpisode ( Show show , List < ShowEpisode > episodes , { int index = 0 } ) async {
QueueSource queueSource = QueueSource (
id: show . id ,
text: show . name ,
source : ' show '
) ;
//Generate media items
List < MediaItem > queue = episodes . map < MediaItem > ( ( e ) = > e . toMediaItem ( show ) ) . toList ( ) ;
//Load and play
await startService ( ) ;
await settings . updateAudioServiceQuality ( ) ;
await setQueueSource ( queueSource ) ;
await AudioService . customAction ( ' setIndex ' , index ) ;
await AudioService . updateQueue ( queue ) ;
if ( ! AudioService . playbackState . playing )
AudioService . play ( ) ;
}
2020-06-23 19:23:12 +00:00
//Load tracks as queue, play track id, set queue source
Future playFromTrackList ( List < Track > tracks , String trackId , QueueSource queueSource ) async {
2020-06-24 14:52:53 +00:00
await startService ( ) ;
2020-06-23 19:23:12 +00:00
List < MediaItem > queue = tracks . map < MediaItem > ( ( track ) = > track . toMediaItem ( ) ) . toList ( ) ;
await setQueueSource ( queueSource ) ;
await _loadQueuePlay ( queue , trackId ) ;
}
//Load smart track list as queue, start from beginning
Future playFromSmartTrackList ( SmartTrackList stl ) async {
//Load from API if no tracks
if ( stl . tracks = = null | | stl . tracks . length = = 0 ) {
if ( settings . offlineMode ) {
Fluttertoast . showToast (
2020-09-18 17:36:41 +00:00
msg: " Offline mode, can't play flow or smart track lists. " . i18n ,
2020-06-23 19:23:12 +00:00
gravity: ToastGravity . BOTTOM ,
toastLength: Toast . LENGTH_SHORT
) ;
return ;
}
//Flow songs cannot be accessed by smart track list call
if ( stl . id = = ' flow ' ) {
stl . tracks = await deezerAPI . flow ( ) ;
} else {
stl = await deezerAPI . smartTrackList ( stl . id ) ;
}
}
QueueSource queueSource = QueueSource (
id: stl . id ,
source : ( stl . id = = ' flow ' ) ? ' flow ' : ' smarttracklist ' ,
2020-10-16 19:06:07 +00:00
text: stl . title ? ? ( ( stl . id = = ' flow ' ) ? ' Flow ' . i18n : ' Smart track list ' . i18n )
2020-06-23 19:23:12 +00:00
) ;
await playFromTrackList ( stl . tracks , stl . tracks [ 0 ] . id , queueSource ) ;
}
Future setQueueSource ( QueueSource queueSource ) async {
2020-06-24 14:52:53 +00:00
await startService ( ) ;
2020-06-23 19:23:12 +00:00
this . queueSource = queueSource ;
await AudioService . customAction ( ' queueSource ' , queueSource . toJson ( ) ) ;
}
2020-10-16 18:54:04 +00:00
//Reorder tracks in queue
Future reorder ( int oldIndex , int newIndex ) async {
await AudioService . customAction ( ' reorder ' , [ oldIndex , newIndex ] ) ;
}
2021-02-09 20:14:14 +00:00
//Start visualizer
Future startVisualizer ( ) async {
await AudioService . customAction ( ' startVisualizer ' ) ;
}
//Stop visualizer
Future stopVisualizer ( ) async {
await AudioService . customAction ( ' stopVisualizer ' ) ;
}
2020-06-23 19:23:12 +00:00
}
void backgroundTaskEntrypoint ( ) async {
AudioServiceBackground . run ( ( ) = > AudioPlayerTask ( ) ) ;
}
class AudioPlayerTask extends BackgroundAudioTask {
2020-10-19 19:28:45 +00:00
AudioPlayer _player ;
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Queue
2020-06-23 19:23:12 +00:00
List < MediaItem > _queue = < MediaItem > [ ] ;
2020-12-27 18:33:59 +00:00
List < MediaItem > _originalQueue ;
bool _shuffle = false ;
2020-08-13 17:39:22 +00:00
int _queueIndex = 0 ;
ConcatenatingAudioSource _audioSource ;
2020-06-23 19:23:12 +00:00
AudioProcessingState _skipState ;
2020-08-13 17:39:22 +00:00
Seeker _seeker ;
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Stream subscriptions
2020-06-23 19:23:12 +00:00
StreamSubscription _eventSub ;
2021-02-09 20:14:14 +00:00
StreamSubscription _audioSessionSub ;
StreamSubscription _visualizerSubscription ;
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Loaded from file/frontend
int mobileQuality ;
int wifiQuality ;
2020-06-23 19:23:12 +00:00
QueueSource queueSource ;
2020-08-13 17:39:22 +00:00
Duration _lastPosition ;
2020-12-14 17:29:28 +00:00
LoopMode _loopMode = LoopMode . off ;
2020-06-23 19:23:12 +00:00
2020-09-18 17:36:41 +00:00
Completer _androidAutoCallback ;
2021-07-02 16:28:59 +00:00
Scrobblenaut _scrobblenaut ;
bool _scrobblenautReady = false ;
// Last logged track id
String _loggedTrackId ;
2020-09-18 17:36:41 +00:00
2020-06-23 19:23:12 +00:00
MediaItem get mediaItem = > _queue [ _queueIndex ] ;
@ override
2020-09-18 17:36:41 +00:00
Future onStart ( Map < String , dynamic > params ) async {
final session = await AudioSession . instance ;
session . configure ( AudioSessionConfiguration . music ( ) ) ;
2020-06-23 19:23:12 +00:00
2020-10-19 19:28:45 +00:00
if ( params [ ' ignoreInterruptions ' ] = = true ) {
_player = AudioPlayer ( handleInterruptions: false ) ;
session . interruptionEventStream . listen ( ( _ ) { } ) ;
session . becomingNoisyEventStream . listen ( ( _ ) { } ) ;
} else
_player = AudioPlayer ( ) ;
2020-08-13 17:39:22 +00:00
//Update track index
_player . currentIndexStream . listen ( ( index ) {
if ( index ! = null ) {
_queueIndex = index ;
AudioServiceBackground . setMediaItem ( mediaItem ) ;
2020-06-23 19:23:12 +00:00
}
} ) ;
2020-08-13 17:39:22 +00:00
//Update state on all clients on change
_eventSub = _player . playbackEventStream . listen ( ( event ) {
2020-10-09 18:52:45 +00:00
//Quality string
if ( _queueIndex ! = - 1 & & _queueIndex < _queue . length ) {
Map extras = mediaItem . extras ;
2020-11-28 21:32:17 +00:00
extras [ ' qualityString ' ] = ' ' ;
2020-10-09 18:52:45 +00:00
_queue [ _queueIndex ] = mediaItem . copyWith ( extras: extras ) ;
}
//Update
2020-08-13 17:39:22 +00:00
_broadcastState ( ) ;
} ) ;
_player . processingStateStream . listen ( ( state ) {
switch ( state ) {
case ProcessingState . completed:
//Player ended, get more songs
2020-10-15 18:37:36 +00:00
if ( _queueIndex = = _queue . length - 1 )
AudioServiceBackground . sendCustomEvent ( {
' action ' : ' queueEnd ' ,
' queueSource ' : ( queueSource ? ? QueueSource ( ) ) . toJson ( )
} ) ;
2020-08-13 17:39:22 +00:00
break ;
case ProcessingState . ready:
//Ready to play
_skipState = null ;
break ;
default :
break ;
}
} ) ;
2020-06-23 19:23:12 +00:00
2021-02-09 20:14:14 +00:00
//Audio session
_audioSessionSub = _player . androidAudioSessionIdStream . listen ( ( event ) {
AudioServiceBackground . sendCustomEvent ( { " action " : ' audioSession ' , " id " : event } ) ;
} ) ;
2020-08-13 17:39:22 +00:00
//Load queue
2020-06-23 19:23:12 +00:00
AudioServiceBackground . setQueue ( _queue ) ;
AudioServiceBackground . sendCustomEvent ( { ' action ' : ' onLoad ' } ) ;
}
@ override
2020-08-13 17:39:22 +00:00
Future onSkipToQueueItem ( String mediaId ) async {
_lastPosition = null ;
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Calculate new index
final newIndex = _queue . indexWhere ( ( i ) = > i . id = = mediaId ) ;
if ( newIndex = = - 1 ) return ;
//Update buffering state
_skipState = newIndex > _queueIndex
? AudioProcessingState . skippingToNext
: AudioProcessingState . skippingToPrevious ;
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Skip in player
await _player . seek ( Duration . zero , index: newIndex ) ;
2020-10-09 18:52:45 +00:00
_queueIndex = newIndex ;
2020-08-13 17:39:22 +00:00
_skipState = null ;
onPlay ( ) ;
2020-06-23 19:23:12 +00:00
}
@ override
2021-07-02 16:28:59 +00:00
Future onPlay ( ) async {
2020-08-13 17:39:22 +00:00
_player . play ( ) ;
//Restore position on play
if ( _lastPosition ! = null ) {
onSeekTo ( _lastPosition ) ;
2020-10-15 20:10:17 +00:00
_lastPosition = null ;
2020-08-13 17:39:22 +00:00
}
2021-07-02 16:28:59 +00:00
//LastFM
if ( _scrobblenautReady & & mediaItem . id ! = _loggedTrackId ) {
_loggedTrackId = mediaItem . id ;
await _scrobblenaut . track . scrobble (
track: mediaItem . title ,
artist: mediaItem . artist ,
album: mediaItem . album ,
) ;
}
2020-06-23 19:23:12 +00:00
}
@ override
2020-08-13 17:39:22 +00:00
Future onPause ( ) = > _player . pause ( ) ;
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future onSeekTo ( Duration pos ) = > _player . seek ( pos ) ;
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future < void > onFastForward ( ) = > _seekRelative ( fastForwardInterval ) ;
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future < void > onRewind ( ) = > _seekRelative ( - rewindInterval ) ;
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future < void > onSeekForward ( bool begin ) async = > _seekContinuously ( begin , 1 ) ;
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future < void > onSeekBackward ( bool begin ) async = > _seekContinuously ( begin , - 1 ) ;
2020-06-23 19:23:12 +00:00
2020-12-14 17:29:28 +00:00
//Remove item from queue
@ override
Future < void > onRemoveQueueItem ( MediaItem mediaItem ) async {
int index = _queue . indexWhere ( ( m ) = > m . id = = mediaItem . id ) ;
_queue . removeAt ( index ) ;
if ( index < = _queueIndex ) {
_queueIndex - - ;
}
_audioSource . removeAt ( index ) ;
AudioServiceBackground . setQueue ( _queue ) ;
}
2020-10-09 18:52:45 +00:00
@ override
Future < void > onSkipToNext ( ) async {
2021-07-02 16:28:59 +00:00
_lastPosition = null ;
2020-10-15 18:37:36 +00:00
if ( _queueIndex = = _queue . length - 1 ) return ;
2020-10-09 18:52:45 +00:00
//Update buffering state
_skipState = AudioProcessingState . skippingToNext ;
_queueIndex + + ;
await _player . seekToNext ( ) ;
_skipState = null ;
await _broadcastState ( ) ;
}
@ override
Future < void > onSkipToPrevious ( ) async {
2020-10-15 18:37:36 +00:00
if ( _queueIndex = = 0 ) return ;
2020-10-09 18:52:45 +00:00
//Update buffering state
_skipState = AudioProcessingState . skippingToPrevious ;
2020-10-12 20:49:13 +00:00
//Normal skip to previous
2020-10-09 18:52:45 +00:00
_queueIndex - - ;
await _player . seekToPrevious ( ) ;
_skipState = null ;
}
2020-09-18 17:36:41 +00:00
@ override
Future < List < MediaItem > > onLoadChildren ( String parentMediaId ) async {
AudioServiceBackground . sendCustomEvent ( {
' action ' : ' screenAndroidAuto ' ,
' id ' : parentMediaId
} ) ;
//Wait for data from main thread
_androidAutoCallback = Completer ( ) ;
List < MediaItem > data = ( await _androidAutoCallback . future ) as List < MediaItem > ;
_androidAutoCallback = null ;
return data ;
}
2020-08-13 17:39:22 +00:00
//While seeking, jump 10s every 1s
void _seekContinuously ( bool begin , int direction ) {
_seeker ? . stop ( ) ;
if ( begin ) {
_seeker = Seeker ( _player , Duration ( seconds: 10 * direction ) , Duration ( seconds: 1 ) , mediaItem ) . . start ( ) ;
}
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
//Relative seek
2020-06-23 19:23:12 +00:00
Future _seekRelative ( Duration offset ) async {
2020-08-13 17:39:22 +00:00
Duration newPos = _player . position + offset ;
//Out of bounds check
2020-06-23 19:23:12 +00:00
if ( newPos < Duration . zero ) newPos = Duration . zero ;
if ( newPos > mediaItem . duration ) newPos = mediaItem . duration ;
2020-08-13 17:39:22 +00:00
await _player . seek ( newPos ) ;
}
//Update state on all clients
Future _broadcastState ( ) async {
await AudioServiceBackground . setState (
controls: [
MediaControl . skipToPrevious ,
if ( _player . playing ) MediaControl . pause else MediaControl . play ,
MediaControl . skipToNext ,
2020-09-13 14:06:12 +00:00
//Stop
MediaControl (
2020-10-19 19:28:45 +00:00
androidIcon: ' drawable/ic_action_stop ' ,
label: ' stop ' ,
action: MediaAction . stop
) ,
2020-08-13 17:39:22 +00:00
] ,
systemActions: [
MediaAction . seekTo ,
MediaAction . seekForward ,
2020-09-09 18:50:15 +00:00
MediaAction . seekBackward ,
2020-10-20 19:55:14 +00:00
MediaAction . stop
2020-08-13 17:39:22 +00:00
] ,
processingState: _getProcessingState ( ) ,
playing: _player . playing ,
position: _player . position ,
bufferedPosition: _player . bufferedPosition ,
speed: _player . speed
) ;
2020-06-24 14:52:53 +00:00
}
2020-08-13 17:39:22 +00:00
//just_audio state -> audio_service state. If skipping, use _skipState
AudioProcessingState _getProcessingState ( ) {
if ( _skipState ! = null ) return _skipState ;
//SRC: audio_service example
switch ( _player . processingState ) {
2020-12-27 18:33:59 +00:00
case ProcessingState . idle:
2020-08-13 17:39:22 +00:00
return AudioProcessingState . stopped ;
case ProcessingState . loading:
return AudioProcessingState . connecting ;
case ProcessingState . buffering:
return AudioProcessingState . buffering ;
case ProcessingState . ready:
return AudioProcessingState . ready ;
case ProcessingState . completed:
return AudioProcessingState . completed ;
default :
throw Exception ( " Invalid state: ${ _player . processingState } " ) ;
2020-06-23 19:23:12 +00:00
}
}
2020-08-13 17:39:22 +00:00
//Replace current queue
2020-06-23 19:23:12 +00:00
@ override
2020-08-13 17:39:22 +00:00
Future onUpdateQueue ( List < MediaItem > q ) async {
2021-07-02 16:28:59 +00:00
_lastPosition = null ;
2020-08-13 17:39:22 +00:00
//just_audio
2021-04-16 18:21:35 +00:00
_shuffle = false ;
_originalQueue = null ;
2020-08-13 17:39:22 +00:00
_player . stop ( ) ;
if ( _audioSource ! = null ) _audioSource . clear ( ) ;
2021-02-09 20:14:14 +00:00
//Filter duplicate IDs
List < MediaItem > queue = [ ] ;
for ( MediaItem mi in q ) {
if ( queue . indexWhere ( ( m ) = > mi . id = = m . id ) = = - 1 )
queue . add ( mi ) ;
}
this . _queue = queue ;
AudioServiceBackground . setQueue ( queue ) ;
2020-08-13 17:39:22 +00:00
//Load
await _loadQueue ( ) ;
2020-10-09 18:52:45 +00:00
//await _player.seek(Duration.zero, index: 0);
2020-08-13 17:39:22 +00:00
}
//Load queue to just_audio
Future _loadQueue ( ) async {
2020-10-09 18:52:45 +00:00
//Don't reset queue index by starting player
int qi = _queueIndex ;
2020-08-13 17:39:22 +00:00
List < AudioSource > sources = [ ] ;
for ( int i = 0 ; i < _queue . length ; i + + ) {
2020-10-14 19:09:16 +00:00
AudioSource s = await _mediaItemToAudioSource ( _queue [ i ] ) ;
if ( s ! = null )
sources . add ( s ) ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
_audioSource = ConcatenatingAudioSource ( children: sources ) ;
//Load in just_audio
try {
2020-12-27 18:33:59 +00:00
await _player . setAudioSource ( _audioSource , initialIndex: qi , initialPosition: Duration . zero ) ;
2020-08-13 17:39:22 +00:00
} catch ( e ) {
//Error loading tracks
}
2020-10-09 18:52:45 +00:00
_queueIndex = qi ;
2020-08-13 17:39:22 +00:00
AudioServiceBackground . setMediaItem ( mediaItem ) ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
Future < AudioSource > _mediaItemToAudioSource ( MediaItem mi ) async {
String url = await _getTrackUrl ( mi ) ;
2020-10-14 19:09:16 +00:00
if ( url = = null ) return null ;
2020-08-13 17:39:22 +00:00
if ( url . startsWith ( ' http ' ) ) return ProgressiveAudioSource ( Uri . parse ( url ) ) ;
2020-12-27 18:33:59 +00:00
return AudioSource . uri ( Uri . parse ( url ) , tag: mi . id ) ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
Future _getTrackUrl ( MediaItem mediaItem , { int quality } ) async {
//Check if offline
String _offlinePath = p . join ( ( await getExternalStorageDirectory ( ) ) . path , ' offline/ ' ) ;
File f = File ( p . join ( _offlinePath , mediaItem . id ) ) ;
if ( await f . exists ( ) ) {
2020-11-28 21:32:17 +00:00
//return f.path;
//Stream server URL
return ' http://localhost:36958/?id= ${ mediaItem . id } ' ;
2020-08-13 17:39:22 +00:00
}
2020-11-28 21:32:17 +00:00
//Show episode direct link
if ( mediaItem . extras [ ' showUrl ' ] ! = null )
return mediaItem . extras [ ' showUrl ' ] ;
2020-08-13 17:39:22 +00:00
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
//This just returns fake url that contains metadata
List playbackDetails = jsonDecode ( mediaItem . extras [ ' playbackDetails ' ] ) ;
//Quality
ConnectivityResult conn = await Connectivity ( ) . checkConnectivity ( ) ;
quality = mobileQuality ;
if ( conn = = ConnectivityResult . wifi ) quality = wifiQuality ;
2020-10-14 19:09:16 +00:00
if ( ( playbackDetails ? ? [ ] ) . length < 2 ) return null ;
2020-11-28 21:32:17 +00:00
//String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
String url = ' http://localhost:36958/?q= $ quality &mv= ${ playbackDetails [ 1 ] } &md5origin= ${ playbackDetails [ 0 ] } &id= ${ mediaItem . id } ' ;
2020-08-13 17:39:22 +00:00
return url ;
}
2020-06-23 19:23:12 +00:00
2020-08-13 17:39:22 +00:00
//Custom actions
2020-06-23 19:23:12 +00:00
@ override
Future onCustomAction ( String name , dynamic args ) async {
2021-02-09 20:14:14 +00:00
switch ( name ) {
case ' updateQuality ' :
//Pass wifi & mobile quality by custom action
//Isolate can't access globals
this . wifiQuality = args [ ' wifiQuality ' ] ;
this . mobileQuality = args [ ' mobileQuality ' ] ;
break ;
//Update queue source
case ' queueSource ' :
this . queueSource = QueueSource . fromJson ( Map < String , dynamic > . from ( args ) ) ;
break ;
//Looping
case ' repeatType ' :
_loopMode = LoopMode . values [ args ] ;
_player . setLoopMode ( _loopMode ) ;
break ;
//Save queue
case ' saveQueue ' :
await this . _saveQueue ( ) ;
break ;
//Load queue after some initialization in frontend
case ' load ' :
await this . _loadQueueFile ( ) ;
break ;
case ' shuffle ' :
String originalId = mediaItem . id ;
if ( ! _shuffle ) {
_shuffle = true ;
_originalQueue = List . from ( _queue ) ;
_queue . shuffle ( ) ;
} else {
_shuffle = false ;
_queue = _originalQueue ;
_originalQueue = null ;
}
2020-12-27 18:33:59 +00:00
2021-02-09 20:14:14 +00:00
//Broken
2020-12-27 18:33:59 +00:00
// _queueIndex = _queue.indexWhere((mi) => mi.id == originalId);
2021-02-09 20:14:14 +00:00
_queueIndex = 0 ;
AudioServiceBackground . setQueue ( _queue ) ;
AudioServiceBackground . setMediaItem ( mediaItem ) ;
await _player . stop ( ) ;
await _loadQueue ( ) ;
await _player . play ( ) ;
break ;
//Android audio callback
case ' screenAndroidAuto ' :
if ( _androidAutoCallback ! = null )
_androidAutoCallback . complete ( jsonDecode ( args ) . map < MediaItem > ( ( m ) = > MediaItem . fromJson ( m ) ) . toList ( ) ) ;
break ;
//Reorder tracks, args = [old, new]
case ' reorder ' :
await _audioSource . move ( args [ 0 ] , args [ 1 ] ) ;
//Switch in queue
List < MediaItem > newQueue = List . from ( _queue ) ;
newQueue . removeAt ( args [ 0 ] ) ;
newQueue . insert ( args [ 1 ] , _queue [ args [ 0 ] ] ) ;
_queue = newQueue ;
//Update UI
AudioServiceBackground . setQueue ( _queue ) ;
_broadcastState ( ) ;
break ;
//Set index without affecting playback for loading
case ' setIndex ' :
this . _queueIndex = args ;
break ;
//Start visualizer
case ' startVisualizer ' :
if ( _visualizerSubscription ! = null ) break ;
_player . startVisualizer (
enableWaveform: false ,
enableFft: true ,
captureRate: 15000 ,
captureSize: 128
) ;
_visualizerSubscription = _player . visualizerFftStream . listen ( ( event ) {
//Calculate actual values
List < double > out = [ ] ;
2021-03-16 19:35:50 +00:00
for ( int i = 0 ; i < event . length / 2 ; i + + ) {
int rfk = event [ i * 2 ] . toSigned ( 8 ) ;
int ifk = event [ i * 2 + 1 ] . toSigned ( 8 ) ;
2021-02-09 20:14:14 +00:00
out . add ( log ( hypot ( rfk , ifk ) + 1 ) / 5.2 ) ;
}
AudioServiceBackground . sendCustomEvent ( { " action " : " visualizer " , " data " : out } ) ;
} ) ;
break ;
//Stop visualizer
case ' stopVisualizer ' :
if ( _visualizerSubscription ! = null ) {
_visualizerSubscription . cancel ( ) ;
_visualizerSubscription = null ;
}
break ;
2021-07-02 16:28:59 +00:00
//Authorize lastfm
case ' authorizeLastFM ' :
String username = args [ 0 ] ;
String password = args [ 1 ] ;
try {
LastFM lastFM = await LastFM . authenticateWithPasswordHash (
apiKey: ' b6ab5ae967bcd8b10b23f68f42493829 ' ,
apiSecret: ' 861b0dff9a8a574bec747f9dab8b82bf ' ,
username: username ,
passwordHash: password
) ;
_scrobblenaut = Scrobblenaut ( lastFM: lastFM ) ;
_scrobblenautReady = true ;
} catch ( e ) { print ( e ) ; }
break ;
case ' disableLastFM ' :
_scrobblenaut = null ;
_scrobblenautReady = false ;
break ;
2020-11-28 21:32:17 +00:00
}
2020-06-23 19:23:12 +00:00
2020-09-18 17:36:41 +00:00
return true ;
2020-06-23 19:23:12 +00:00
}
@ override
2020-08-13 17:39:22 +00:00
Future onTaskRemoved ( ) async {
await onStop ( ) ;
2020-06-23 19:23:12 +00:00
}
@ override
2020-08-13 17:39:22 +00:00
Future onClose ( ) async {
2020-08-16 20:17:22 +00:00
print ( ' onClose ' ) ;
2020-06-23 19:23:12 +00:00
await onStop ( ) ;
}
2020-08-13 17:39:22 +00:00
Future onStop ( ) async {
await _saveQueue ( ) ;
_player . stop ( ) ;
if ( _eventSub ! = null ) _eventSub . cancel ( ) ;
2021-02-09 20:14:14 +00:00
if ( _audioSessionSub ! = null ) _audioSessionSub . cancel ( ) ;
2020-06-23 19:23:12 +00:00
2020-08-16 20:17:22 +00:00
await super . onStop ( ) ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
//Get queue save file path
2020-06-23 19:23:12 +00:00
Future < String > _getQueuePath ( ) async {
Directory dir = await getApplicationDocumentsDirectory ( ) ;
2020-08-13 17:39:22 +00:00
return p . join ( dir . path , ' playback.json ' ) ;
2020-06-23 19:23:12 +00:00
}
//Export queue to JSON
Future _saveQueue ( ) async {
2020-10-09 18:52:45 +00:00
if ( _queueIndex = = 0 & & _queue . length = = 0 ) return ;
2020-08-13 17:39:22 +00:00
String path = await _getQueuePath ( ) ;
File f = File ( path ) ;
2020-10-09 18:52:45 +00:00
//Create if doesn't exist
2020-08-13 17:39:22 +00:00
if ( ! await File ( path ) . exists ( ) ) {
f = await f . create ( ) ;
}
Map data = {
2020-06-23 19:23:12 +00:00
' index ' : _queueIndex ,
' queue ' : _queue . map < Map < String , dynamic > > ( ( mi ) = > mi . toJson ( ) ) . toList ( ) ,
2020-08-13 17:39:22 +00:00
' position ' : _player . position . inMilliseconds ,
2020-06-23 19:23:12 +00:00
' queueSource ' : ( queueSource ? ? QueueSource ( ) ) . toJson ( ) ,
2020-12-14 17:29:28 +00:00
' loopMode ' : LoopMode . values . indexOf ( _loopMode ? ? LoopMode . off )
2020-08-13 17:39:22 +00:00
} ;
await f . writeAsString ( jsonEncode ( data ) ) ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
//Restore queue & playback info from path
Future _loadQueueFile ( ) async {
2020-06-23 19:23:12 +00:00
File f = File ( await _getQueuePath ( ) ) ;
if ( await f . exists ( ) ) {
Map < String , dynamic > json = jsonDecode ( await f . readAsString ( ) ) ;
this . _queue = ( json [ ' queue ' ] ? ? [ ] ) . map < MediaItem > ( ( mi ) = > MediaItem . fromJson ( mi ) ) . toList ( ) ;
2020-08-13 17:39:22 +00:00
this . _queueIndex = json [ ' index ' ] ? ? 0 ;
2020-06-23 19:23:12 +00:00
this . _lastPosition = Duration ( milliseconds: json [ ' position ' ] ? ? 0 ) ;
this . queueSource = QueueSource . fromJson ( json [ ' queueSource ' ] ? ? { } ) ;
2020-12-14 17:29:28 +00:00
this . _loopMode = LoopMode . values [ ( json [ ' loopMode ' ] ? ? 0 ) ] ;
2020-08-13 17:39:22 +00:00
//Restore queue
2020-06-23 19:23:12 +00:00
if ( _queue ! = null ) {
2020-08-13 17:39:22 +00:00
await AudioServiceBackground . setQueue ( _queue ) ;
await _loadQueue ( ) ;
2020-10-09 18:52:45 +00:00
await AudioServiceBackground . setMediaItem ( mediaItem ) ;
2020-06-23 19:23:12 +00:00
}
}
2020-08-13 17:39:22 +00:00
//Send restored queue source to ui
AudioServiceBackground . sendCustomEvent ( {
' action ' : ' onRestore ' ,
2020-12-14 17:29:28 +00:00
' queueSource ' : ( queueSource ? ? QueueSource ( ) ) . toJson ( ) ,
' loopMode ' : LoopMode . values . indexOf ( _loopMode )
2020-08-13 17:39:22 +00:00
} ) ;
return true ;
2020-06-23 19:23:12 +00:00
}
2020-08-13 17:39:22 +00:00
@ override
Future onAddQueueItemAt ( MediaItem mi , int index ) async {
//-1 == play next
if ( index = = - 1 ) index = _queueIndex + 1 ;
_queue . insert ( index , mi ) ;
await AudioServiceBackground . setQueue ( _queue ) ;
2020-10-14 19:09:16 +00:00
AudioSource _newSource = await _mediaItemToAudioSource ( mi ) ;
if ( _newSource ! = null )
await _audioSource . insert ( index , _newSource ) ;
2020-08-13 17:39:22 +00:00
_saveQueue ( ) ;
}
//Add at end of queue
@ override
Future onAddQueueItem ( MediaItem mi ) async {
2021-02-09 20:14:14 +00:00
if ( _queue . indexWhere ( ( m ) = > m . id = = mi . id ) ! = - 1 )
return ;
2020-08-13 17:39:22 +00:00
_queue . add ( mi ) ;
await AudioServiceBackground . setQueue ( _queue ) ;
2020-10-14 19:09:16 +00:00
AudioSource _newSource = await _mediaItemToAudioSource ( mi ) ;
if ( _newSource ! = null )
await _audioSource . add ( _newSource ) ;
2020-08-13 17:39:22 +00:00
_saveQueue ( ) ;
}
@ override
Future onPlayFromMediaId ( String mediaId ) async {
2020-09-18 17:36:41 +00:00
//Android auto load tracks
if ( mediaId . startsWith ( AndroidAuto . prefix ) ) {
AudioServiceBackground . sendCustomEvent ( {
' action ' : ' tracksAndroidAuto ' ,
' id ' : mediaId . replaceFirst ( AndroidAuto . prefix , ' ' )
} ) ;
return ;
}
2020-08-13 17:39:22 +00:00
//Does the same thing
await this . onSkipToQueueItem ( mediaId ) ;
}
}
//Seeker from audio_service example (why reinvent the wheel?)
//While holding seek button, will continuously seek
class Seeker {
final AudioPlayer player ;
final Duration positionInterval ;
final Duration stepInterval ;
final MediaItem mediaItem ;
bool _running = false ;
Seeker ( this . player , this . positionInterval , this . stepInterval , this . mediaItem ) ;
Future start ( ) async {
_running = true ;
while ( _running ) {
Duration newPosition = player . position + positionInterval ;
if ( newPosition < Duration . zero ) newPosition = Duration . zero ;
if ( newPosition > mediaItem . duration ) newPosition = mediaItem . duration ;
player . seek ( newPosition ) ;
await Future . delayed ( stepInterval ) ;
}
}
void stop ( ) {
_running = false ;
}
2020-09-01 14:41:15 +00:00
}