Last active
August 12, 2021 17:47
-
-
Save unlimitedprograming/88334ef4a541f9c025065cb6c11cb1f9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:audio_service/audio_service.dart'; | |
import 'package:just_audio/just_audio.dart'; | |
import 'package:audio_session/audio_session.dart'; | |
/// This task defines logic for playing a list of podcast episodes. | |
class AudioPlayerTask extends BackgroundAudioTask { | |
AudioPlayer _player = new AudioPlayer(); | |
AudioProcessingState _skipState; | |
Seeker _seeker; | |
StreamSubscription<PlaybackEvent> _eventSubscription; | |
List<MediaItem> _queue = []; | |
List<MediaItem> get queue => _queue; | |
int get index => _player.playbackEvent.currentIndex; | |
MediaItem get mediaItem => index == null ? null : queue[index]; | |
@override | |
Future<void> onStart(Map<String, dynamic> params) async { | |
_loadMediaItemsIntoQueue(params); | |
await _setAudioSession(); | |
_propogateEventsFromAudioPlayerToAudioServiceClients(); | |
_performSpecialProcessingForStateTransistions(); | |
_loadQueue(); | |
} | |
void _loadMediaItemsIntoQueue(Map<String, dynamic> params) { | |
_queue.clear(); | |
final List mediaItems = params['data']; | |
for (var item in mediaItems) { | |
final mediaItem = MediaItem.fromJson(item); | |
_queue.add(mediaItem); | |
} | |
} | |
Future<void> _setAudioSession() async { | |
final session = await AudioSession.instance; | |
await session.configure(AudioSessionConfiguration.music()); | |
} | |
void _propogateEventsFromAudioPlayerToAudioServiceClients() { | |
_eventSubscription = _player.playbackEventStream.listen((event) { | |
_broadcastState(); | |
}); | |
} | |
void _performSpecialProcessingForStateTransistions() { | |
_player.processingStateStream.listen((state) { | |
switch (state) { | |
case ProcessingState.completed: | |
onPause(); | |
break; | |
case ProcessingState.ready: | |
_skipState = null; | |
break; | |
default: | |
break; | |
} | |
}); | |
} | |
Future<void> _loadQueue() async { | |
AudioServiceBackground.setQueue(queue); | |
try { | |
await _player.setAudioSource(ConcatenatingAudioSource( | |
children: | |
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(), | |
)); | |
_player.durationStream.listen((duration) { | |
_updateQueueWithCurrentDuration(duration); | |
}); | |
// onPlay(); | |
} catch (e) { | |
print('Error: $e'); | |
// onStop(); | |
} | |
} | |
void _updateQueueWithCurrentDuration(Duration duration) { | |
final songIndex = _player.playbackEvent.currentIndex; | |
final modifiedMediaItem = mediaItem.copyWith(duration: duration); | |
queue[songIndex] = modifiedMediaItem; | |
AudioServiceBackground.setMediaItem(queue[songIndex]); | |
AudioServiceBackground.setQueue(queue); | |
} | |
@override | |
Future<void> onSkipToQueueItem(String mediaId) async { | |
// Then default implementations of onSkipToNext and onSkipToPrevious will | |
// delegate to this method. | |
final newIndex = queue.indexWhere((item) => item.id == mediaId); | |
if (newIndex == -1) return; | |
// During a skip, the player may enter the buffering state. We could just | |
// propagate that state directly to AudioService clients but AudioService | |
// has some more specific states we could use for skipping to next and | |
// previous. This variable holds the preferred state to send instead of | |
// buffering during a skip, and it is cleared as soon as the player exits | |
// buffering (see the listener in onStart). | |
_skipState = newIndex > index | |
? AudioProcessingState.skippingToNext | |
: AudioProcessingState.skippingToPrevious; | |
// This jumps to the beginning of the queue item at newIndex. | |
_player.seek(Duration.zero, index: newIndex); | |
// Demonstrate custom events. | |
AudioServiceBackground.sendCustomEvent('skip to $newIndex'); | |
} | |
@override | |
Future<void> onPlay() => _player.play(); | |
@override | |
Future<void> onPause() => _player.pause(); | |
@override | |
Future<void> onSeekTo(Duration position) => _player.seek(position); | |
@override | |
Future<void> onFastForward() => _seekRelative(fastForwardInterval); | |
@override | |
Future<void> onRewind() => _seekRelative(-rewindInterval); | |
@override | |
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1); | |
@override | |
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1); | |
@override | |
Future<void> onStop() async { | |
await _player.dispose(); | |
_eventSubscription.cancel(); | |
// It is important to wait for this state to be broadcast before we shut | |
// down the task. If we don't, the background task will be destroyed before | |
// the message gets sent to the UI. | |
await _broadcastState(); | |
// Shut down this task | |
await super.onStop(); | |
} | |
@override | |
Future<void> onCustomAction(String type, dynamic mediaId) async { | |
if(type == 'updateMedia') { | |
final newIndex = queue.indexWhere((item) => item.id == mediaId); | |
if (newIndex == -1) return; | |
_player.seek(Duration.zero, index: newIndex); | |
if (!_player.playing) _player.play(); | |
AudioServiceBackground.sendCustomEvent('goto to $newIndex'); | |
} | |
} | |
/// Jumps away from the current position by [offset]. | |
Future<void> _seekRelative(Duration offset) async { | |
var newPosition = _player.position + offset; | |
// Make sure we don't jump out of bounds. | |
if (newPosition < Duration.zero) newPosition = Duration.zero; | |
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration; | |
// Perform the jump via a seek. | |
await _player.seek(newPosition); | |
} | |
/// Begins or stops a continuous seek in [direction]. After it begins it will | |
/// continue seeking forward or backward by 10 seconds within the audio, at | |
/// intervals of 1 second in app time. | |
void _seekContinuously(bool begin, int direction) { | |
_seeker?.stop(); | |
if (begin) { | |
_seeker = Seeker(_player, Duration(seconds: 10 * direction), | |
Duration(seconds: 1), mediaItem) | |
..start(); | |
} | |
} | |
/// Broadcasts the current state to all clients. | |
Future<void> _broadcastState() async { | |
await AudioServiceBackground.setState( | |
controls: [ | |
MediaControl.skipToPrevious, | |
if (_player.playing) MediaControl.pause else MediaControl.play, | |
MediaControl.stop, | |
MediaControl.skipToNext, | |
], | |
systemActions: [ | |
MediaAction.seekTo, | |
MediaAction.seekForward, | |
MediaAction.seekBackward, | |
], | |
androidCompactActions: [0, 1, 3], | |
processingState: _getProcessingState(), | |
playing: _player.playing, | |
position: _player.position, | |
bufferedPosition: _player.bufferedPosition, | |
speed: _player.speed, | |
); | |
} | |
/// Maps just_audio's processing state into into audio_service's playing | |
/// state. If we are in the middle of a skip, we use [_skipState] instead. | |
AudioProcessingState _getProcessingState() { | |
if (_skipState != null) return _skipState; | |
switch (_player.processingState) { | |
case ProcessingState.idle: | |
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}"); | |
} | |
} | |
} | |
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, | |
); | |
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); | |
} | |
} | |
stop() { | |
_running = false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment