Skip to content

Instantly share code, notes, and snippets.

@unlimitedprograming
Last active August 12, 2021 17:47
Show Gist options
  • Save unlimitedprograming/88334ef4a541f9c025065cb6c11cb1f9 to your computer and use it in GitHub Desktop.
Save unlimitedprograming/88334ef4a541f9c025065cb6c11cb1f9 to your computer and use it in GitHub Desktop.
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