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 _dragPositionSubject = BehaviorSubject.seeded(null); MainScreen() { AudioService.start( backgroundTaskEntrypoint: _iosNotificationCenterInitTaskEntrypoint); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Audio Service Demo'), ), body: Center( child: StreamBuilder( 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 get _screenStateStream => Rx.combineLatest3, 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( _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 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( 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 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 onSkipToNext() => _skip(1); @override Future onSkipToPrevious() => _skip(-1); Future _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 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 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 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(); } } void _iosNotificationCenterInitTaskEntrypoint() async { AudioServiceBackground.run(() => IosNotificationCenterInitTask()); } class IosNotificationCenterInitTask extends BackgroundAudioTask { @override Future onStart() async { await AudioServiceBackground.setState( controls: [], basicState: BasicPlaybackState.playing, ); } @override Future onStop() async {} }