audio_service 0.8.0 audio_service: ^0.8.0 copied to clipboard
Flutter plugin to play audio in the background while the screen is off.
import 'dart:math';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_action_play_arrow',
label: 'Play',
action: MediaAction.play,
);
MediaControl pauseControl = MediaControl(
androidIcon: 'drawable/ic_action_pause',
label: 'Pause',
action: MediaAction.pause,
);
MediaControl skipToNextControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_next',
label: 'Next',
action: MediaAction.skipToNext,
);
MediaControl skipToPreviousControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_previous',
label: 'Previous',
action: MediaAction.skipToPrevious,
);
MediaControl stopControl = MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'Stop',
action: MediaAction.stop,
);
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Audio Service Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: AudioServiceWidget(child: MainScreen()),
);
}
}
class MainScreen extends StatelessWidget {
/// Tracks the position while the user drags the seek bar.
final BehaviorSubject<double> _dragPositionSubject =
BehaviorSubject.seeded(null);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Audio Service Demo'),
),
body: Center(
child: StreamBuilder<ScreenState>(
stream: _screenStateStream,
builder: (context, snapshot) {
final screenState = snapshot.data;
final queue = screenState?.queue;
final mediaItem = screenState?.mediaItem;
final state = screenState?.playbackState;
final basicState = state?.basicState ?? BasicPlaybackState.none;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (queue != null && queue.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.skip_previous),
iconSize: 64.0,
onPressed: mediaItem == queue.first
? null
: AudioService.skipToPrevious,
),
IconButton(
icon: Icon(Icons.skip_next),
iconSize: 64.0,
onPressed: mediaItem == queue.last
? null
: AudioService.skipToNext,
),
],
),
if (mediaItem?.title != null) Text(mediaItem.title),
if (basicState == BasicPlaybackState.none) ...[
audioPlayerButton(),
textToSpeechButton(),
] else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (basicState == BasicPlaybackState.playing)
pauseButton()
else if (basicState == BasicPlaybackState.paused)
playButton()
else if (basicState == BasicPlaybackState.buffering ||
basicState == BasicPlaybackState.skippingToNext ||
basicState == BasicPlaybackState.skippingToPrevious)
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
),
),
stopButton(),
],
),
if (basicState != BasicPlaybackState.none &&
basicState != BasicPlaybackState.stopped) ...[
positionIndicator(mediaItem, state),
Text("State: " +
"$basicState".replaceAll(RegExp(r'^.*\.'), '')),
StreamBuilder(
stream: AudioService.customEventStream,
builder: (context, snapshot) {
return Text("custom event: ${snapshot.data}");
},
),
],
],
);
},
),
),
);
}
/// Encapsulate all the different data we're interested in into a single
/// stream so we don't have to nest StreamBuilders.
Stream<ScreenState> get _screenStateStream =>
Rx.combineLatest3<List<MediaItem>, MediaItem, PlaybackState, ScreenState>(
AudioService.queueStream,
AudioService.currentMediaItemStream,
AudioService.playbackStateStream,
(queue, mediaItem, playbackState) =>
ScreenState(queue, mediaItem, playbackState));
RaisedButton audioPlayerButton() => startButton(
'AudioPlayer',
() {
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
notificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
enableQueue: true,
);
},
);
RaisedButton textToSpeechButton() => startButton(
'TextToSpeech',
() {
AudioService.start(
backgroundTaskEntrypoint: _textToSpeechTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
notificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
);
},
);
RaisedButton startButton(String label, VoidCallback onPressed) =>
RaisedButton(
child: Text(label),
onPressed: onPressed,
);
IconButton playButton() => IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: AudioService.play,
);
IconButton pauseButton() => IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: AudioService.pause,
);
IconButton stopButton() => IconButton(
icon: Icon(Icons.stop),
iconSize: 64.0,
onPressed: AudioService.stop,
);
Widget positionIndicator(MediaItem mediaItem, PlaybackState state) {
double seekPos;
return StreamBuilder(
stream: Rx.combineLatest2<double, double, double>(
_dragPositionSubject.stream,
Stream.periodic(Duration(milliseconds: 200)),
(dragPosition, _) => dragPosition),
builder: (context, snapshot) {
double position = snapshot.data ?? state.currentPosition.toDouble();
double duration = mediaItem?.duration?.toDouble();
return Column(
children: [
if (duration != null)
Slider(
min: 0.0,
max: duration,
value: seekPos ?? max(0.0, min(position, duration)),
onChanged: (value) {
_dragPositionSubject.add(value);
},
onChangeEnd: (value) {
AudioService.seekTo(value.toInt());
// Due to a delay in platform channel communication, there is
// a brief moment after releasing the Slider thumb before the
// new position is broadcast from the platform side. This
// hack is to hold onto seekPos until the next state update
// comes through.
// TODO: Improve this code.
seekPos = value;
_dragPositionSubject.add(null);
},
),
Text("${(state.currentPosition / 1000).toStringAsFixed(3)}"),
],
);
},
);
}
}
class ScreenState {
final List<MediaItem> queue;
final MediaItem mediaItem;
final PlaybackState playbackState;
ScreenState(this.queue, this.mediaItem, this.playbackState);
}
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
final _queue = <MediaItem>[
MediaItem(
id: "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3",
album: "Science Friday",
title: "A Salute To Head-Scratching Science",
artist: "Science Friday and WNYC Studios",
duration: 5739820,
artUri:
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg",
),
MediaItem(
id: "https://s3.amazonaws.com/scifri-segments/scifri201711241.mp3",
album: "Science Friday",
title: "From Cat Rheology To Operatic Incompetence",
artist: "Science Friday and WNYC Studios",
duration: 2856950,
artUri:
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg",
),
];
int _queueIndex = -1;
AudioPlayer _audioPlayer = new AudioPlayer();
Completer _completer = Completer();
BasicPlaybackState _skipState;
bool _playing;
bool get hasNext => _queueIndex + 1 < _queue.length;
bool get hasPrevious => _queueIndex > 0;
MediaItem get mediaItem => _queue[_queueIndex];
BasicPlaybackState _eventToBasicState(AudioPlaybackEvent event) {
if (event.buffering) {
return BasicPlaybackState.buffering;
} else {
switch (event.state) {
case AudioPlaybackState.none:
return BasicPlaybackState.none;
case AudioPlaybackState.stopped:
return BasicPlaybackState.stopped;
case AudioPlaybackState.paused:
return BasicPlaybackState.paused;
case AudioPlaybackState.playing:
return BasicPlaybackState.playing;
case AudioPlaybackState.connecting:
return _skipState ?? BasicPlaybackState.connecting;
case AudioPlaybackState.completed:
return BasicPlaybackState.stopped;
default:
throw Exception("Illegal state");
}
}
}
@override
Future<void> onStart() async {
var playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_handlePlaybackCompleted();
});
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
final state = _eventToBasicState(event);
if (state != BasicPlaybackState.stopped) {
_setState(
state: state,
position: event.position.inMilliseconds,
);
}
});
AudioServiceBackground.setQueue(_queue);
await onSkipToNext();
await _completer.future;
playerStateSubscription.cancel();
eventSubscription.cancel();
}
void _handlePlaybackCompleted() {
if (hasNext) {
onSkipToNext();
} else {
onStop();
}
}
void playPause() {
if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
onPause();
else
onPlay();
}
@override
Future<void> onSkipToNext() => _skip(1);
@override
Future<void> onSkipToPrevious() => _skip(-1);
Future<void> _skip(int offset) async {
final newPos = _queueIndex + offset;
if (!(newPos >= 0 && newPos < _queue.length)) return;
if (_playing == null) {
// First time, we want to start playing
_playing = true;
} else if (_playing) {
// Stop current item
await _audioPlayer.stop();
}
// Load next item
_queueIndex = newPos;
AudioServiceBackground.setMediaItem(mediaItem);
_skipState = offset > 0
? BasicPlaybackState.skippingToNext
: BasicPlaybackState.skippingToPrevious;
await _audioPlayer.setUrl(mediaItem.id);
_skipState = null;
// Resume playback if we were playing
if (_playing) {
onPlay();
} else {
_setState(state: BasicPlaybackState.paused);
}
}
@override
void onPlay() {
if (_skipState == null) {
_playing = true;
_audioPlayer.play();
AudioServiceBackground.sendCustomEvent('just played');
}
}
@override
void onPause() {
if (_skipState == null) {
_playing = false;
_audioPlayer.pause();
AudioServiceBackground.sendCustomEvent('just paused');
}
}
@override
void onSeekTo(int position) {
_audioPlayer.seek(Duration(milliseconds: position));
}
@override
void onClick(MediaButton button) {
playPause();
}
@override
Future<void> onStop() async {
await _audioPlayer.stop();
await _audioPlayer.dispose();
_setState(state: BasicPlaybackState.stopped);
_completer.complete();
}
void _setState({@required BasicPlaybackState state, int position}) {
if (position == null) {
position = _audioPlayer.playbackEvent.position.inMilliseconds;
}
AudioServiceBackground.setState(
controls: getControls(state),
systemActions: [MediaAction.seekTo],
basicState: state,
position: position,
);
}
List<MediaControl> getControls(BasicPlaybackState state) {
if (_playing) {
return [
skipToPreviousControl,
pauseControl,
stopControl,
skipToNextControl
];
} else {
return [
skipToPreviousControl,
playControl,
stopControl,
skipToNextControl
];
}
}
}
void _textToSpeechTaskEntrypoint() async {
AudioServiceBackground.run(() => TextPlayerTask());
}
class TextPlayerTask extends BackgroundAudioTask {
FlutterTts _tts = FlutterTts();
/// Represents the completion of a period of playing or pausing.
Completer _playPauseCompleter = Completer();
/// This wraps [_playPauseCompleter.future], replacing [_playPauseCompleter]
/// if it has already completed.
Future _playPauseFuture() {
if (_playPauseCompleter.isCompleted) _playPauseCompleter = Completer();
return _playPauseCompleter.future;
}
BasicPlaybackState get _basicState => AudioServiceBackground.state.basicState;
@override
Future<void> onStart() async {
playPause();
for (var i = 1; i <= 10 && _basicState != BasicPlaybackState.stopped; i++) {
AudioServiceBackground.setMediaItem(mediaItem(i));
AudioServiceBackground.androidForceEnableMediaButtons();
_tts.speak('$i');
// Wait for the speech or a pause request.
await Future.any(
[Future.delayed(Duration(seconds: 1)), _playPauseFuture()]);
// If we were just paused...
if (_playPauseCompleter.isCompleted &&
_basicState == BasicPlaybackState.paused) {
// Wait to be unpaused...
await _playPauseFuture();
}
}
if (_basicState != BasicPlaybackState.stopped) onStop();
}
MediaItem mediaItem(int number) => MediaItem(
id: 'tts_$number',
album: 'Numbers',
title: 'Number $number',
artist: 'Sample Artist');
void playPause() {
if (_basicState == BasicPlaybackState.playing) {
_tts.stop();
AudioServiceBackground.setState(
controls: [playControl, stopControl],
basicState: BasicPlaybackState.paused,
);
} else {
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
basicState: BasicPlaybackState.playing,
);
}
_playPauseCompleter.complete();
}
@override
void onPlay() {
playPause();
}
@override
void onPause() {
playPause();
}
@override
void onClick(MediaButton button) {
playPause();
}
@override
void onStop() {
if (_basicState == BasicPlaybackState.stopped) return;
_tts.stop();
AudioServiceBackground.setState(
controls: [],
basicState: BasicPlaybackState.stopped,
);
_playPauseCompleter.complete();
}
}