2020-10-09 18:52:45 +00:00
import ' dart:math ' ;
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 ' ;
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-06-23 19:23:12 +00:00
import ' definitions.dart ' ;
import ' ../settings.dart ' ;
import ' dart:io ' ;
import ' dart:async ' ;
import ' dart:convert ' ;
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-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 ;
if ( event [ ' action ' ] = = ' onLoad ' ) {
//After audio_service is loaded, load queue, set quality
await settings . updateAudioServiceQuality ( ) ;
await AudioService . customAction ( ' load ' ) ;
return ;
}
if ( event [ ' action ' ] = = ' onRestore ' ) {
//Load queueSource from isolate
this . queueSource = QueueSource . fromJson ( event [ ' queueSource ' ] ) ;
}
if ( event [ ' action ' ] = = ' queueEnd ' ) {
//If last song is played, load more queue
this . queueSource = QueueSource . fromJson ( event [ ' queueSource ' ] ) ;
2020-10-15 18:37:36 +00:00
onQueueEnd ( ) ;
2020-06-23 19:23:12 +00:00
return ;
}
2020-09-18 17:36:41 +00:00
//Android auto get screen
if ( event [ ' action ' ] = = ' screenAndroidAuto ' ) {
AndroidAuto androidAuto = AndroidAuto ( ) ;
List < MediaItem > data = await androidAuto . getScreen ( event [ ' id ' ] ) ;
await AudioService . customAction ( ' screenAndroidAuto ' , jsonEncode ( data ) ) ;
}
//Android auto play list
if ( event [ ' action ' ] = = ' tracksAndroidAuto ' ) {
AndroidAuto androidAuto = AndroidAuto ( ) ;
await androidAuto . playItem ( event [ ' id ' ] ) ;
}
2020-06-23 19:23:12 +00:00
} ) ;
_playbackStateStreamSubscription = AudioService . playbackStateStream . listen ( ( event ) {
//Log song (if allowed)
if ( event = = null ) return ;
if ( event . processingState = = AudioProcessingState . ready & & event . playing ) {
2020-10-09 18:52:45 +00:00
if ( settings . logListen ) {
//Check if duplicate
if ( cache . loggedTrackId = = AudioService . currentMediaItem . id ) return ;
cache . loggedTrackId = AudioService . currentMediaItem . id ;
deezerAPI . logListen ( AudioService . currentMediaItem . id ) ;
}
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 ;
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-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-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 ( ) ;
await AudioService . updateQueue ( queue ) ;
2020-10-19 19:28:45 +00:00
if ( queue [ 0 ] . id ! = trackId )
await AudioService . skipToQueueItem ( trackId ) ;
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 ;
case ' smartradio ' : //SmartRadio/Artist radio
tracks = await deezerAPI . smartRadio ( queueSource . id ) ;
break ;
case ' libraryshuffle ' : //Library shuffle
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 :
print ( queueSource . toJson ( ) ) ;
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 ) ;
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 '
) ) ;
}
//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 ] ) ;
}
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-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 ;
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-06-23 19:23:12 +00:00
2020-09-18 17:36:41 +00:00
Completer _androidAutoCallback ;
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 ;
extras [ ' qualityString ' ] = event . qualityString ? ? ' ' ;
_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
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
2020-08-13 17:39:22 +00:00
Future onPlay ( ) {
_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
}
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-10-09 18:52:45 +00:00
@ override
Future < void > onSkipToNext ( ) async {
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 > d ata = ( 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 ) {
case ProcessingState . none:
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 {
//just_audio
_player . stop ( ) ;
if ( _audioSource ! = null ) _audioSource . clear ( ) ;
//audio_service
this . _queue = q ;
AudioServiceBackground . setQueue ( _queue ) ;
//Load
2020-10-09 18:52:45 +00:00
_queueIndex = 0 ;
2020-08-13 17:39:22 +00:00
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 {
await _player . load ( _audioSource ) ;
2020-10-09 18:52:45 +00:00
await _player . seek ( Duration . zero , index: qi ) ;
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 ) ) ;
return AudioSource . uri ( Uri . parse ( url ) ) ;
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 ( ) ) {
return f . path ;
}
//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-08-13 17:39:22 +00:00
String url = ' https://dzcdn.net/?md5= ${ playbackDetails [ 0 ] } &mv= ${ playbackDetails [ 1 ] } &q= ${ quality . toString ( ) } # ${ mediaItem . id } ' ;
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 {
if ( name = = ' updateQuality ' ) {
//Pass wifi & mobile quality by custom action
//Isolate can't access globals
this . wifiQuality = args [ ' wifiQuality ' ] ;
this . mobileQuality = args [ ' mobileQuality ' ] ;
}
//Change queue source
if ( name = = ' queueSource ' ) {
this . queueSource = QueueSource . fromJson ( Map < String , dynamic > . from ( args ) ) ;
}
2020-08-13 17:39:22 +00:00
//Looping
2020-06-23 19:23:12 +00:00
if ( name = = ' repeatType ' ) {
2020-08-13 17:39:22 +00:00
_player . setLoopMode ( LoopMode . values [ args ] ) ;
2020-06-23 19:23:12 +00:00
}
2020-10-12 20:49:13 +00:00
if ( name = = ' saveQueue ' )
await this . _saveQueue ( ) ;
2020-08-13 17:39:22 +00:00
//Load queue after some initialization in frontend
2020-10-12 20:49:13 +00:00
if ( name = = ' load ' )
await this . _loadQueueFile ( ) ;
2020-08-13 17:39:22 +00:00
//Shuffle
2020-09-22 17:13:54 +00:00
if ( name = = ' shuffle ' ) {
2020-10-15 18:37:36 +00:00
_queue . shuffle ( ) ;
AudioServiceBackground . setQueue ( _queue ) ;
_queueIndex = 0 ;
await _loadQueue ( ) ;
2020-09-22 17:13:54 +00:00
}
2020-09-18 17:36:41 +00:00
//Android auto callback
if ( name = = ' screenAndroidAuto ' & & _androidAutoCallback ! = null ) {
_androidAutoCallback . complete ( jsonDecode ( args ) . map < MediaItem > ( ( m ) = > MediaItem . fromJson ( m ) ) . toList ( ) ) ;
2020-06-23 19:23:12 +00:00
}
2020-10-16 18:54:04 +00:00
//Reorder tracks, args = [old, new]
if ( name = = ' 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 ( ) ;
}
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 ( ) ;
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-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-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 ' ,
' queueSource ' : ( queueSource ? ? QueueSource ( ) ) . toJson ( )
} ) ;
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 {
_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
}